Skip to content

Incorrect union type inferred #17930

New issue

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

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

Already on GitHub? # to your account

Closed
pelotom opened this issue Aug 20, 2017 · 11 comments
Closed

Incorrect union type inferred #17930

pelotom opened this issue Aug 20, 2017 · 11 comments
Labels
Fixed A PR has been merged for this issue

Comments

@pelotom
Copy link

pelotom commented Aug 20, 2017

I've found it frequently desirable to be able to "look up" the union variant associated with a type tag. For example, given a union

type Foo = { tag: 'n'; val: number } | { tag: 's'; val: string }

one wants to be able to write a type operator

type Lookup<T extends Foo['tag']> = // ???

such that Lookup<'n'> = number and Lookup<'s'> = string.

I don't think this is possible in the current type system (would welcome correction on that point). Instead, we can use the trick of starting with a Lookup type, and deriving the union type from it:

type Lookup = {
  n: number
  s: string
}

type Foo = {
  [T in keyof Lookup]: { tag: T; val: Lookup[T] }
}[keyof Lookup]

then the inferred type of Foo is

type Foo = {
    tag: "n";
    val: number;
} | {
    tag: "s";
    val: string;
}

as desired, and we have our lookup type: Lookup['n'] = number and Lookup['s'] = string.

So I use this pattern a lot, and I wanted to generalize it:

type Unionize<Lookup> = {
  [T in keyof Lookup]: { tag: T; val: Lookup[T] }
}[keyof Lookup]

Unfortunately, this doesn't do what you want:

type Foo = Unionize<{
  n: number
  s: string
}>

Here, Foo is inferred to be

type Foo = {
    tag: "n" | "s";
    val: string | number;
}

This seems like a bug!

@jcalz
Copy link
Contributor

jcalz commented Aug 20, 2017

Looking at quickinfo on Unionize<Lookup>, I see:

type Unionize<Lookup> = { tag: keyof Lookup; val: Lookup[keyof Lookup]; }

which is "simplified" in the same surprising/incorrect way I see in #17908. I think these are the same underlying issue.

@gcanti
Copy link

gcanti commented Aug 20, 2017

Workaround?

type Unionize<Lookup, X = { [T in keyof Lookup]: { tag: T; val: Lookup[T] } }> = X[keyof X]

@gcnew
Copy link
Contributor

gcnew commented Aug 20, 2017

@gcanti workaround is ingenious. The default forces type evaluation.

The first report of this issue seems to be #15756.

@pelotom
Copy link
Author

pelotom commented Aug 20, 2017

Wow, very cool @gcanti!

I didn't realize type parameter defaults could reference previous type parameters. This opens up a whole new world...

@pelotom
Copy link
Author

pelotom commented Aug 21, 2017

Using @gcanti's nifty type parameter default trick I put together a new typescript library, unionize, which allows generating tagged unions along with associated creation functions, predicates and match functions. Would welcome any feedback!

@mhegazy
Copy link
Contributor

mhegazy commented Aug 22, 2017

Just gotta say @gcanti's proposal is impressive :)

This seems like a bug!

This is the expected behavior. mapped types are a transformation on properties of a type, they do not generate a union type. Many operations that happen on mapped types e.g. inference, rely on the fact that they are homomorphic transformation.

@mhegazy mhegazy added the Working as Intended The behavior described is the intended behavior; this is not a bug label Aug 22, 2017
@jcalz
Copy link
Contributor

jcalz commented Aug 22, 2017

@mhegazy: It may be expected behavior if you know how mapped types are implemented, but it's very surprising, as it violates the seemingly airtight principle of composition:

type X = ...
type F<T> = ...
type G<T> = ...
type FoG<T> = F<G<T>>
type FGX1 = F<G<X>> 
type FGX2 = FoG<X> 
// FGX1 should be equivalent to FGX2

Can you explain more about why generating a union type (i.e., T[A|B]T[A]|T[B]) breaks a constraint about mapped types?

Thanks!

@gcnew
Copy link
Contributor

gcnew commented Aug 22, 2017

For completeness' sake/History, the workaround was first found by @nirendy / @tycho01 during their exploration in #12215 (ref #12215 (comment), #16018 (comment)).

@jcalz
Copy link
Contributor

jcalz commented Aug 25, 2017

Is this really "working as intended" in light of #18042 as a fix to #15756 ?

@mhegazy mhegazy added Fixed A PR has been merged for this issue and removed Working as Intended The behavior described is the intended behavior; this is not a bug labels Aug 25, 2017
@mhegazy
Copy link
Contributor

mhegazy commented Aug 26, 2017

unionize should be working now after #15756.

@jcalz
Copy link
Contributor

jcalz commented Feb 12, 2018

FYI to anyone who gets here, #21316 now allows you to look up a union type by tag value.

# for free to subscribe to this conversation on GitHub. Already have an account? #.
Labels
Fixed A PR has been merged for this issue
Projects
None yet
Development

No branches or pull requests

5 participants