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

Incorrect union type inferred #17930

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.

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

No branches or pull requests

5 participants