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

Mapped types behave strangely on primitive types #13351

Closed
danielearwicker opened this issue Jan 8, 2017 · 5 comments
Closed

Mapped types behave strangely on primitive types #13351

danielearwicker opened this issue Jan 8, 2017 · 5 comments
Assignees
Labels
Bug A bug in TypeScript Fixed A PR has been merged for this issue

Comments

@danielearwicker
Copy link

Given:

type Meta<T> = {
    readonly[P in keyof T]: {
        value: T[P];
        readonly children: Meta<T[P]>;
    };
}

I'd expect children to either be the empty type {} (if T has no properties) or to only have properties of the shape { value: T, children: { ... } }.

But instead, for Meta<boolean>, children is apparently boolean, like the definition of Meta is simply ignored and replaced with its type argument. Same substitution occurs for number, string, null, undefined and never. What's the reasoning behind this? It seems inconsistent.

@ahejlsberg
Copy link
Member

See #12447. It generally isn't meaningful to apply mapped types to primitives, but in recursive types such as your example it is going to happen since types are rarely "objects all the way down". Ideally we'd have something like conditional types (#12424) that would allow you to provide alternative behavior for primitive types.

@ahejlsberg ahejlsberg added the Question An issue which isn't directly actionable in code label Jan 8, 2017
@danielearwicker
Copy link
Author

Thanks. Conditionals would be great (e.g. test if T[K] is readonly to decide whether a corresponding property should also be readonly).

But re:

"Furthermore, when a primitive type is substituted for T in an isomorphic mapped type, we simply produce that primitive type."

My question is, what use case does that fit? I'm finding that "no properties" would be better mapped to "no properties."

@ahejlsberg
Copy link
Member

My question is, what use case does that fit? I'm finding that "no properties" would be better mapped to "no properties."

I think undefined and null are particularly compelling examples for the current behavior. You really don't want those mapped to empty object types. Some of the examples in #12447 illustrate that.

@danielearwicker
Copy link
Author

Hmm, maybe I'm misinterpreting this feature! :) I thought in this context an interface becomes a compile-time "dictionary of string to type", for use in meta-programming. keyof T is a collection of property keys (possibly empty). T[K] fetches a type by string, and [P in K] is like comprehension/projection over the list.

The special case for primitive T seems to rip through these assumptions, throwing away the type expression that mentions T[K]. This is meta information about the structure of types.

By including the input type in the output it seems like levels are getting mixed. I'm generating an output type (with a structure I specify), derived from information about an input type, so the raw input information has no business appearing directly in the output.

I experimented with adding a second type parameter:

type Meta<T, A> = {
    readonly[P in keyof T]: {
        value: T[P];
        also: A;
        readonly children: Meta<T[P], A>;
    };
}

interface Input {
    x: string;
    y: number;
}

declare const output: Meta<Input, boolean>;

const shouldFail: { important: boolean } = output.x.children;

Now the type of output.x.children is any, so this in fact compiles. (I have --noImplicitAny switched on).

@ahejlsberg
Copy link
Member

Hmm, maybe I'm misinterpreting this feature!

No, it turns out we have a bug related to instantiation of mapped types in contexts with type parameters other than the one being mapped. Your example should be an error (and will be once we fix the bug).

@ahejlsberg ahejlsberg added Bug A bug in TypeScript and removed Question An issue which isn't directly actionable in code labels Jan 9, 2017
@ahejlsberg ahejlsberg self-assigned this Jan 9, 2017
@ahejlsberg ahejlsberg added the Fixed A PR has been merged for this issue label Jan 9, 2017
@microsoft microsoft locked and limited conversation to collaborators Jun 19, 2018
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
Bug A bug in TypeScript Fixed A PR has been merged for this issue
Projects
None yet
Development

No branches or pull requests

2 participants