Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Bug: DeepReadonly<T> and DeepWritable<T> are not exact inverses if T is a type parameter #369

Open
polyipseity opened this issue May 3, 2023 · 3 comments
Labels
bug Something isn't working help wanted Extra attention is needed

Comments

@polyipseity
Copy link
Contributor

Versions

package version
typescript 5.0.4
ts-essentials 9.3.2

Repro Code

import type { DeepReadonly, DeepWritable } from 'ts-essentials'

declare function read<T>(value: DeepReadonly<T>): void
declare function write<T>(value: DeepWritable<T>): void
type ReadonlyWritable<T> = DeepReadonly<DeepWritable<T>>
type WriteableReadonly<T> = DeepWritable<DeepReadonly<T>>
function readwrite<T>(rw: ReadonlyWritable<T>, wr: WriteableReadonly<T>) {
    read<T>(rw)
    write<T>(wr)
}

TypeScript Playground

https://www.typescriptlang.org/play?#code/JYWwDg9gTgLgBDAnmApnA3nAIilYBKKAhgCYQB2ANogDTa5gDqUwMRARpWgL5wBmUCCDgByGAGcAtCnHiU5GMCKVxIgLAAoTSRQBjSkSho+AV3K7FFOEdIAeACoA+ABQA3ZSZQAuenkKkKagdHAEofVwhgEm09AyN+MwtgKwB3FhgUYLcPb18mdI4uYLC4CKjNJFQ4fzIqRGZWQsynOABePJrAxFscPAa2TmbHRwrkNH7iQc664La8-qaehmmgpxGNU3NLcmtiEjTWIecoFJ8V+oLB4Lo0nwmm8+KMTThX3bsnY5SQl7eDjKyaR+Gm4miAA

Expected Result

DeepReadonly<DeepWritable<T>> should be assignable to DeepReadonly<T> and DeepWritable<DeepReadonly<T>> should be assignable to DeepWritable<T>.

Actual Result

The above are not actually assignable if T depends on any type parameter (if T is fully concrete then it works as expected). It would be nice if this also works for type parameters.

However, I am not sure if this is even possible in TypeScript (the type system might not be powerful enough, or that checking for such type equality with a free parameter is undecidable), and it might be more work than what it is worth.

A more practical workaround I am using right now is manually doing the conversion using a overloaded function, which you may be interested in:

export function simplifyType<T>(value: DeepWritable<DeepReadonly<T>>): DeepWritable<T>
export function simplifyType<T>(value: DeepReadonly<DeepWritable<T>>): DeepReadonly<T>
export function simplifyType<T>(value: T): T { return value }
@polyipseity polyipseity added the bug Something isn't working label May 3, 2023
@polyipseity polyipseity changed the title Bug: DeepReadonly and DeepWritable are not exact inverses Bug: DeepReadonly<T> and DeepWritable<T> are not exact inverses if T is a type parameter May 4, 2023
@Beraliv
Copy link
Collaborator

Beraliv commented May 22, 2023

Hey @polyipseity

Thank you for reporting this issue

I won' be available to look at it until beginning of June, I will come back to you once I have some time

Apologies in advance

@Beraliv
Copy link
Collaborator

Beraliv commented Apr 27, 2024

Hey @polyipseity

I don't have any ideas about how to fix it yet. To keep assignability, we have to add T & <do something else with T> which breaks both utility types

If you have any suggestions, I'm open to experiments. Have you seen it working on other type libraries?

@Beraliv Beraliv added the help wanted Extra attention is needed label Apr 27, 2024
@polyipseity
Copy link
Contributor Author

Unfortunately, I don't have any good suggestions. My closest idea would be:

type DeepWritable2<T> = T extends DeepReadonly<infer U> ? DeepWritable<U> : DeepWritable<T>
type DeepReadonly2<T> = T extends DeepWritable<infer U> ? DeepReadonly<U> : DeepReadonly<T>
type ReadonlyWritable2<T> = DeepReadonly2<DeepWritable2<T>>
type WriteableReadonly2<T> = DeepWritable2<DeepReadonly2<T>>
function readwrite2<T>(rw: ReadonlyWritable2<T>, wr: WriteableReadonly2<T>) {
    read<T>(rw)
    write<T>(wr)
}

But TypeScript does not evaluate the type conditional in DeepWriteable2 and DeepReadonly2 if there is a type parameter (T) when checking assignability, so it simply fails.

If only there is something that can force evaluation of type conditionals in TypeScript, something like microsoft/TypeScript#47980 (comment), which only works when there are no type parameters (i.e., the T in Expand<T> is a concrete type).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
bug Something isn't working help wanted Extra attention is needed
Projects
None yet
Development

No branches or pull requests

2 participants