Skip to content

Nested generic object of unions is not assignable to equivalent union of nested generic objects #51318

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

Closed
sluukkonen opened this issue Oct 26, 2022 · 4 comments
Labels
Not a Defect This behavior is one of several equally-correct options

Comments

@sluukkonen
Copy link

sluukkonen commented Oct 26, 2022

Bug Report

πŸ”Ž Search Terms

union, generic, nested, nested, complex

πŸ•— Version & Regression Information

  • This is the behavior in every version I tried. I read through the FAQ and couldn't find relevant sections.

⏯ Playground Link

Playground link with relevant code

πŸ’» Code

type Test<T> = {
    test: T
}

declare let outer: Test<Test<"a">> | Test<Test<"b">>
declare let inner: Test<Test<"a" | "b">>

inner = outer // this is accepted
outer = inner // this is not

πŸ™ Actual behavior

The assignment outer = inner fails following error

Type 'Test<Test<"a" | "b">>' is not assignable to type 'Test<Test<"a">> | Test<Test<"b">>'.
  Type 'Test<Test<"a" | "b">>' is not assignable to type 'Test<Test<"a">>'.
    Type 'Test<"a" | "b">' is not assignable to type 'Test<"a">'.
      Type '"a" | "b"' is not assignable to type '"a"'.
        Type '"b"' is not assignable to type '"a"'.(2322)

πŸ™‚ Expected behavior

As far as I can tell, it should be accepted, but the type checker isn't quite smart enough. This issue came up in a larger real world example, but this was the minimal reproduction I could think of.

@MartinJohns
Copy link
Contributor

MartinJohns commented Oct 26, 2022

In one case you have an object that can either store a or b, in the other case you either have an object that can only store a, or an object that can only store b. The types are not equivalent.

Although the assignment of outer to inner is already unsound.

@sluukkonen
Copy link
Author

Perhaps this is a more motivating example. I'm basically trying to use a custom result type that tracks possible errors, a bit like Haskell's Either or Rust's Result. However, when trying to use it in practice, the ergonomics isn't great, since TypeScript requires me to add quite a bit of type annotations to make the code compile.

interface Success<T> {
    ok: true
    value: T
}

interface Failure<E> {
    ok: false
    error: E
}

type Result<T, E> = Success<T> | Failure<E>

const success = <T>(value: T): Success<T> => ({ok: true, value})
const failure = <E>(error: E): Failure<E> => ({ok: false, error})

declare function test<T, E>(fn: (value: unknown) => Result<T, E>): Result<T, E>

// This works, for whatever reason.
test((val) => {
    if (typeof val !== "string") return failure("not a string" as const)
    if (val.length === 0) return failure("empty string" as const)
    return success(val)
})

// This doesn't work. Why is that?
test((val) => {
    if (typeof val !== "string") return failure({code: "not a string"} as const)
    if (val.length === 0) return failure({code: "empty string"} as const)
    return success(val)
})

// With an explicit type annotation, everything is ok.
test((val): Result<string, {code: "not a string" | "empty string"}> => {
    if (typeof val !== "string") return failure({code: "not a string"} as const)
    if (val.length === 0) return failure({code: "empty string"} as const)
    return success(val)
})

@RyanCavanaugh
Copy link
Member

A few things to discuss.

First, for types with a single value of their type parameter, indeed X<T | U> === X<T> | X<U>. TS is incomplete on this measure because it, in general, will instead rely on comparison using variance measurement when comparing two instantiations of the same generic type rather than structural comparison. However, using variance is absolutely critical for performance reasons. Figuring out what correctly constitutes a "single value" is also pretty tricky once you move beyond simple properties.

Second, in general, generic inference won't collect two disparate candidates and then infer to a union of them. This makes generics for the purposes of input validation largely useless, e.g. a call to something like find<T>(someNumberArray, someString) should not make a (sound!) inference of T = string | number, since reuse of a type parameter is intended to indicate at least some linkage of types. Callers intending that behavior can and should write find<string | number>(... in that case.

The middle test call is, from inference's perspective, an equivalent situation. There are two candidates to collect for E with no common type amongst them, but TS isn't willing to construct a union between them (nor would that union even work, given the lack of the single-use equivalence reduction rule noted above).

@RyanCavanaugh RyanCavanaugh added the Not a Defect This behavior is one of several equally-correct options label Oct 27, 2022
@github-actions
Copy link

github-actions bot commented Jun 8, 2023

This issue has been marked as 'Not a Defect' and has seen no recent activity. It has been automatically closed for house-keeping purposes.

@github-actions github-actions bot closed this as not planned Won't fix, can't repro, duplicate, stale Jun 8, 2023
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Not a Defect This behavior is one of several equally-correct options
Projects
None yet
Development

No branches or pull requests

3 participants