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

Surprising/incorrect composition of generic type functions (when one is a mapped type) #17908

Closed
jcalz opened this issue Aug 18, 2017 · 8 comments
Labels
Fixed A PR has been merged for this issue

Comments

@jcalz
Copy link
Contributor

jcalz commented Aug 18, 2017

When trying to compose generic type aliases I've run into some instances where the compiler breaks the intended semantics. Here is as small/self-contained an example as I can come up with:

TypeScript Version: 2.4.0 / nightly (2.5.0-dev.20170627)

Code

// Let's define some generic type aliases:

// union of values of T:
type Valueof<T> = T[keyof T];

// mapped keys of properties:
type MappedKeyof<T> = {
  [K in keyof T]: keyof T[K]
}

// and a concrete type to act on:
type Foo = {
  one: { prop1: string },
  two: { prop2: number }
}

// We want the union of all keys of properties of Foo, namely: 'prop1' | 'prop2'
type KeyofPropertyofFoo = Valueof<MappedKeyof<Foo>> // 'prop1' | 'prop2'; correct

// hey, this "union of keys of properties of" operation would be useful for me,
// let's define the composition as follows:
type KeyofPropertyof<T> = Valueof<MappedKeyof<T>>;

// apply it to foo, and... it is broken!
type KeyofPropertyofFooBroken = KeyofPropertyof<Foo> // never; broken

Expected behavior:
I expect KeyofPropertyof<Foo> to be equivalent to Valueof<MappedKeyof<Foo>>.

Actual behavior:
What's KeyofPropertyof<T> doing? Quickinfo says type KeyofPropertyof<T> = keyof (T[keyof T]). So it took Valueof<MappedKeyof<T>> and "simplified" it to keyof Valueof<T>, but that's the intersection of the keys of the properties, not the union. Hence the never instead of 'prop1'|'prop2'🙁


In general, given

type A = ...
type F<T> = ...
type G<T> = ...
type FoG<T> = F<G<T>>

I expect F<G<A>> to be equivalent to FoG<A>. Anything else makes it very hard to reason about what is going on.

This seems like something @tycho01 has run into before: is it the same issue as #16244 and/or #16018? Is it a bug? design limitation? intention? there any way to work around it in general? My use cases here are mostly type manipulation in the absence of built-in subtraction/exact/conditional mapped types. Someone on Stack Overflow asks a question, I try to build something that solves the problem, and I hit this.

Thoughts? Thanks!

@KiaraGrouwstra
Copy link
Contributor

type KeyofPropertyofFooBroken = KeyofPropertyof<Foo>; // never; broken
function the<T, V extends T>() {} // check type matches without having to name your types
the<never, string>(); // correctly errors with ... does not satisfy ...
the<Valueof<KeyofPropertyof<Foo>>, 'prop1' | 'prop2'>(); // LHS would be `never` yet doesn't error?

That's definitely odd. It's never yet it isn't.

@jcalz
Copy link
Contributor Author

jcalz commented Aug 19, 2017

I'm not sure why you did

the<Valueof<KeyofPropertyof<Foo>>, 'prop1' | 'prop2'>(); // LHS is any

since that's something like Valueof<never> which evaluates to any (interesting that explicitly writing never[keyof never] is an error, but still evaluates to any).

If you do

the<KeyofPropertyof<Foo>, 'prop1' | 'prop2'>(); // LHS is 'never' and does error

it does indicate that KeyofPropertyof<Foo> is never... as I reported, but which I don't think it should be, as above.

@KiaraGrouwstra
Copy link
Contributor

explicitly writing never[keyof never] is an error, but still evaluates to any

I think that's the default type on error.

Your test is definitely better to verify the issue, yeah. 😅

@jcalz
Copy link
Contributor Author

jcalz commented Aug 20, 2017

Ah, applying the amazing workaround by @gcanti:

type KeyofPropertyof<T, X=MappedKeyof<T>> = Valueof<X>;
type KeyofPropertyofFoo = KeyofPropertyof<Foo> // 'prop1'|'prop2'

And so, in general, the workaround for

type A = ...
type F<T> = ...
type G<T> = ...
type FoG<T> = F<G<T>> //broken

is

type FoG<T, GT=G<T>> = F<GT> // works

I guess I can live with this if it can't be fixed otherwise, although it's confusing enough that I'd be loath to introduce it in an answer to someone's question on SO.

@KiaraGrouwstra
Copy link
Contributor

Yeah still seems a bug. Sorry I hadn't suggested it, presumed you'd tried since it was raised in both of the other threads you linked. :P

@jcalz
Copy link
Contributor Author

jcalz commented Aug 20, 2017

Indeed. I guess I didn't read them well enough. 🚫👀❗️

And yeah, the workaround is not helping me in the more general issue that I pared down to the above snippet. Seems like I keep having to break things apart into multiple steps and introduce concrete types early in order to build something, which is bad for users of a library.

@jcalz
Copy link
Contributor Author

jcalz commented Aug 25, 2017

Hmm, since #15756 is now fixed, I wonder if this issue is also?

@KiaraGrouwstra
Copy link
Contributor

@jcalz: just tried on master -- it is!

@jcalz jcalz closed this as completed Aug 28, 2017
@mhegazy mhegazy added the Fixed A PR has been merged for this issue label Aug 29, 2017
@mhegazy mhegazy added this to the TypeScript 2.6 milestone Aug 29, 2017
@microsoft microsoft locked and limited conversation to collaborators Jun 14, 2018
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

3 participants