Skip to content

Extending mapped type doesn't allow extra properties when extended type has string literal values #27707

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
asmundg opened this issue Oct 11, 2018 · 6 comments
Assignees
Labels
Bug A bug in TypeScript Domain: Mapped Types The issue relates to mapped types Needs More Info The issue still hasn't been fully clarified

Comments

@asmundg
Copy link
Member

asmundg commented Oct 11, 2018

TypeScript Version: 3.2.0-dev.20181011

Search Terms: extends mapped literal widening

Code

// Type '{ a: string; c: number; }' is not assignable to type '{ a: "a"; }'.
//   Object literal may only specify known properties, and 'c' does not exist in type '{ a: "a"; }'. [2322]
const f: <T extends { [P in keyof T]: { a: "a" } }>(literal: T) => void = () => {}
f({one: {a: "a", c: 1}})

// works
const a: "a" = "a"
f({one: {a, c: 1}})

// works
const f_: <T extends { [P in keyof T]: { a: string } }>(literal: T) => void = () => {}
f_({one: {a: "a", c: 1}})

// works
const f__: <T extends {one: { a: "a" }}>(literal: T) => void = () => {}
f__({one: {a: "a", c: 1}})

Expected behavior:

Code compiles for all cases.

Actual behavior:

Code fails when extending a mapped type with string literal properties:

test.ts:4:18 - error TS2322: Type '{ a: string; c: number; }' is not assignable to type '{ a: "a"; }'.
  Object literal may only specify known properties, and 'c' does not exist in type '{ a: "a"; }'.

Removing either the mapping, the string literal or the widenable input makes the code pass type checking.

Also notable is that the one.a property of the function argument is only inferred to be defined by the extended type in the first case. E.g. jumping to definition only does something in the first f() call.

This seems related to the a property being widened when mapped over.

Playground Link: http://www.typescriptlang.org/play/#src=%2F%2F%20Type%20'%7B%20a%3A%20string%3B%20c%3A%20number%3B%20%7D'%20is%20not%20assignable%20to%20type%20'%7B%20a%3A%20%22a%22%3B%20%7D'.%0D%0A%2F%2F%20%20%20Object%20literal%20may%20only%20specify%20known%20properties%2C%20and%20'c'%20does%20not%20exist%20in%20type%20'%7B%20a%3A%20%22a%22%3B%20%7D'.%20%5B2322%5D%0D%0Aconst%20f%3A%20%3CT%20extends%20%7B%20%5BP%20in%20keyof%20T%5D%3A%20%7B%20a%3A%20%22a%22%20%7D%20%7D%3E(literal%3A%20T)%20%3D%3E%20void%20%3D%20()%20%3D%3E%20%7B%7D%0D%0Af(%7Bone%3A%20%7Ba%3A%20%22a%22%2C%20c%3A%201%7D%7D)%0D%0A%0D%0A%2F%2F%20works%0D%0Aconst%20a%3A%20%22a%22%20%3D%20%22a%22%0D%0Af(%7Bone%3A%20%7Ba%3A%20a%2C%20c%3A%201%7D%7D)%0D%0A%0D%0A%2F%2F%20works%0D%0Aconst%20f_%3A%20%3CT%20extends%20%7B%20%5BP%20in%20keyof%20T%5D%3A%20%7B%20a%3A%20string%20%7D%20%7D%3E(literal%3A%20T)%20%3D%3E%20void%20%3D%20()%20%3D%3E%20%7B%7D%0D%0Af_(%7Bone%3A%20%7Ba%3A%20%22a%22%2C%20c%3A%201%7D%7D)%0D%0A%0D%0A%2F%2F%20works%0D%0Aconst%20f__%3A%20%3CT%20extends%20%7Bone%3A%20%7B%20a%3A%20%22a%22%20%7D%7D%3E(literal%3A%20T)%20%3D%3E%20void%20%3D%20()%20%3D%3E%20%7B%7D%0D%0Af__(%7Bone%3A%20%7Ba%3A%20%22a%22%2C%20c%3A%201%7D%7D)%0D%0A

Related Issues:

@DanielRosenwasser DanielRosenwasser added Bug A bug in TypeScript Domain: Mapped Types The issue relates to mapped types labels Oct 11, 2018
@DanielRosenwasser DanielRosenwasser added this to the TypeScript 3.2 milestone Oct 11, 2018
@asmundg
Copy link
Member Author

asmundg commented Oct 12, 2018

Possibly related: #27421

@sandersn
Copy link
Member

How does T extends { [P in keyof T]: { a: "a" } } differ from T extends { [s: string]: { a: "a" } in the real code that led to this issue? (In your example, you even just use T, so I assume that the real code behind this issue is more complex.)

Note that from the type system's perspective, T extends { [P in keyof T]: { a: "a" } } is circular and so some things don't work the way you would expect.

@sandersn sandersn added the Needs More Info The issue still hasn't been fully clarified label Oct 12, 2018
@asmundg
Copy link
Member Author

asmundg commented Oct 12, 2018

The original problem is in react-native's typings (https://github.com/DefinitelyTyped/DefinitelyTyped/blob/master/types/react-native/index.d.ts#L5222) which uses the first form and has some weird behavior related to inference.

I had a hard time describing the problem, so I tried to narrow it down a bit.

The working variants were a result of messing around to try to isolate the issue.

@zhigang1992
Copy link

zhigang1992 commented Oct 12, 2018

@asmundg I think it might actually be a type definition issue.

Update updating node_modules/@types/react-native/index.d.ts with

-    type NamedStyles<T> = { [P in keyof T]: ViewStyle | TextStyle | ImageStyle };
+    type NamedStyles = { [key: string]: ViewStyle | TextStyle | ImageStyle };

The issue seems to be resolved locally

@asmundg
Copy link
Member Author

asmundg commented Oct 12, 2018

As you point out in the DefinitelyTyped PR, extending {[key: string]: ...} now requires the type parameter in export function create<T extends NamedStyles<T>>(styles: T): { [P in keyof T]: RegisteredStyle<T[P]> }; to have a string index, precluding the use of a plain interface without the index.

It seems like the pattern the code aims for is "for each property in an object, the value type must extend V", which seems difficult do do without referring to T or requiring an index signature. Or requiring T to be explicitly provided instead of letting it be inferred.

@sandersn
Copy link
Member

I'm closing this issue since the underlying issue is By Design for Typescript itself, and a PR for a fix of @types/react-native is open at DefinitelyTyped/DefinitelyTyped#30564.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Bug A bug in TypeScript Domain: Mapped Types The issue relates to mapped types Needs More Info The issue still hasn't been fully clarified
Projects
None yet
Development

No branches or pull requests

4 participants