From cfd97da680623585d5fcc2e41e8ebdb3c97b23fe Mon Sep 17 00:00:00 2001 From: Nathan Shively-Sanders Date: Mon, 16 Oct 2017 13:31:23 -0700 Subject: [PATCH 1/3] Infer keyof from string literals + contravariantly 1. When inferring from a string literal [union] type to a index type, infer a object type with properties whose names match the constituents of the union type. The type of all the properties is `{}`. 2. Infer index types contravariantly. For example, when inferring `"foo" | "bar"` to `keyof T`, the inference algorithm now infers `{ foo: {}, bar: {} }` to `T`. When inferring `string` to `keyof T`, the inference algorithm now infers `{ [s: string]: {} }` to `T`. --- src/compiler/checker.ts | 38 ++++++++++++++++++++++++++++++++++---- 1 file changed, 34 insertions(+), 4 deletions(-) diff --git a/src/compiler/checker.ts b/src/compiler/checker.ts index 3208615755227..6f092825a7b70 100644 --- a/src/compiler/checker.ts +++ b/src/compiler/checker.ts @@ -10648,10 +10648,32 @@ namespace ts { return type === typeParameter || type.flags & TypeFlags.UnionOrIntersection && forEach((type).types, t => isTypeParameterAtTopLevel(t, typeParameter)); } - // Infer a suitable input type for a homomorphic mapped type { [P in keyof T]: X }. We construct - // an object type with the same set of properties as the source type, where the type of each - // property is computed by inferring from the source property type to X for the type - // variable T[P] (i.e. we treat the type T[P] as the type variable we're inferring for). + /** Create an object with properties named in the string literal type. Every property has type `{}` */ + function createEmptyObjectTypeFromStringLiteral(type: Type) { + const members = createSymbolTable(); + forEachType(type, t => { + if (!(t.flags & TypeFlags.StringLiteral)) { + return; + } + const name = escapeLeadingUnderscores((t as StringLiteralType).value); + const literalProp = createSymbol(SymbolFlags.Property, name); + literalProp.type = emptyObjectType; + if (t.symbol) { + literalProp.declarations = t.symbol.declarations; + literalProp.valueDeclaration = t.symbol.valueDeclaration; + } + members.set(name, literalProp); + }); + const indexInfo = type.flags & TypeFlags.String ? createIndexInfo(emptyObjectType, /*isReadonly*/ false) : undefined + return createAnonymousType(undefined, members, emptyArray, emptyArray, indexInfo, undefined); + } + + /** + * Infer a suitable input type for a homomorphic mapped type { [P in keyof T]: X }. We construct + * an object type with the same set of properties as the source type, where the type of each + * property is computed by inferring from the source property type to X for the type + * variable T[P] (i.e. we treat the type T[P] as the type variable we're inferring for). + */ function inferTypeForHomomorphicMappedType(source: Type, target: MappedType): Type { const properties = getPropertiesOfType(source); let indexInfo = getIndexInfoOfType(source, IndexKind.String); @@ -10805,7 +10827,15 @@ namespace ts { } } else if (source.flags & TypeFlags.Index && target.flags & TypeFlags.Index) { + priority ^= InferencePriority.Contravariant; inferFromTypes((source).type, (target).type); + priority ^= InferencePriority.Contravariant; + } + else if ((isLiteralType(source) || source.flags & TypeFlags.String) && target.flags & TypeFlags.Index) { + const empty = createEmptyObjectTypeFromStringLiteral(source); + priority ^= InferencePriority.Contravariant; + inferFromTypes(empty, (target as IndexType).type); + priority ^= InferencePriority.Contravariant; } else if (source.flags & TypeFlags.IndexedAccess && target.flags & TypeFlags.IndexedAccess) { inferFromTypes((source).objectType, (target).objectType); From cd1be1bbe3ef2c39ed9ce56fe2585ba4c59e1d87 Mon Sep 17 00:00:00 2001 From: Nathan Shively-Sanders Date: Mon, 16 Oct 2017 13:35:12 -0700 Subject: [PATCH 2/3] Test:type inference from strings to keyof T succeeds --- ...inferObjectTypeFromStringLiteralToKeyof.js | 8 ++++++ ...ObjectTypeFromStringLiteralToKeyof.symbols | 22 +++++++++++++++ ...erObjectTypeFromStringLiteralToKeyof.types | 27 +++++++++++++++++++ ...inferObjectTypeFromStringLiteralToKeyof.ts | 3 +++ 4 files changed, 60 insertions(+) create mode 100644 tests/baselines/reference/inferObjectTypeFromStringLiteralToKeyof.js create mode 100644 tests/baselines/reference/inferObjectTypeFromStringLiteralToKeyof.symbols create mode 100644 tests/baselines/reference/inferObjectTypeFromStringLiteralToKeyof.types create mode 100644 tests/cases/compiler/inferObjectTypeFromStringLiteralToKeyof.ts diff --git a/tests/baselines/reference/inferObjectTypeFromStringLiteralToKeyof.js b/tests/baselines/reference/inferObjectTypeFromStringLiteralToKeyof.js new file mode 100644 index 0000000000000..6a9a081ae6056 --- /dev/null +++ b/tests/baselines/reference/inferObjectTypeFromStringLiteralToKeyof.js @@ -0,0 +1,8 @@ +//// [inferObjectTypeFromStringLiteralToKeyof.ts] +declare function inference(target: T, name: keyof T): void; +declare var two: "a" | "d"; +inference({ a: 1, b: 2, c: 3, d(n) { return n } }, two); + + +//// [inferObjectTypeFromStringLiteralToKeyof.js] +inference({ a: 1, b: 2, c: 3, d: function (n) { return n; } }, two); diff --git a/tests/baselines/reference/inferObjectTypeFromStringLiteralToKeyof.symbols b/tests/baselines/reference/inferObjectTypeFromStringLiteralToKeyof.symbols new file mode 100644 index 0000000000000..aa3c9e89f9a42 --- /dev/null +++ b/tests/baselines/reference/inferObjectTypeFromStringLiteralToKeyof.symbols @@ -0,0 +1,22 @@ +=== tests/cases/compiler/inferObjectTypeFromStringLiteralToKeyof.ts === +declare function inference(target: T, name: keyof T): void; +>inference : Symbol(inference, Decl(inferObjectTypeFromStringLiteralToKeyof.ts, 0, 0)) +>T : Symbol(T, Decl(inferObjectTypeFromStringLiteralToKeyof.ts, 0, 27)) +>target : Symbol(target, Decl(inferObjectTypeFromStringLiteralToKeyof.ts, 0, 30)) +>T : Symbol(T, Decl(inferObjectTypeFromStringLiteralToKeyof.ts, 0, 27)) +>name : Symbol(name, Decl(inferObjectTypeFromStringLiteralToKeyof.ts, 0, 40)) +>T : Symbol(T, Decl(inferObjectTypeFromStringLiteralToKeyof.ts, 0, 27)) + +declare var two: "a" | "d"; +>two : Symbol(two, Decl(inferObjectTypeFromStringLiteralToKeyof.ts, 1, 11)) + +inference({ a: 1, b: 2, c: 3, d(n) { return n } }, two); +>inference : Symbol(inference, Decl(inferObjectTypeFromStringLiteralToKeyof.ts, 0, 0)) +>a : Symbol(a, Decl(inferObjectTypeFromStringLiteralToKeyof.ts, 2, 11)) +>b : Symbol(b, Decl(inferObjectTypeFromStringLiteralToKeyof.ts, 2, 17)) +>c : Symbol(c, Decl(inferObjectTypeFromStringLiteralToKeyof.ts, 2, 23)) +>d : Symbol(d, Decl(inferObjectTypeFromStringLiteralToKeyof.ts, 2, 29)) +>n : Symbol(n, Decl(inferObjectTypeFromStringLiteralToKeyof.ts, 2, 32)) +>n : Symbol(n, Decl(inferObjectTypeFromStringLiteralToKeyof.ts, 2, 32)) +>two : Symbol(two, Decl(inferObjectTypeFromStringLiteralToKeyof.ts, 1, 11)) + diff --git a/tests/baselines/reference/inferObjectTypeFromStringLiteralToKeyof.types b/tests/baselines/reference/inferObjectTypeFromStringLiteralToKeyof.types new file mode 100644 index 0000000000000..39c4bb84196b8 --- /dev/null +++ b/tests/baselines/reference/inferObjectTypeFromStringLiteralToKeyof.types @@ -0,0 +1,27 @@ +=== tests/cases/compiler/inferObjectTypeFromStringLiteralToKeyof.ts === +declare function inference(target: T, name: keyof T): void; +>inference : (target: T, name: keyof T) => void +>T : T +>target : T +>T : T +>name : keyof T +>T : T + +declare var two: "a" | "d"; +>two : "a" | "d" + +inference({ a: 1, b: 2, c: 3, d(n) { return n } }, two); +>inference({ a: 1, b: 2, c: 3, d(n) { return n } }, two) : void +>inference : (target: T, name: keyof T) => void +>{ a: 1, b: 2, c: 3, d(n) { return n } } : { a: number; b: number; c: number; d(n: any): any; } +>a : number +>1 : 1 +>b : number +>2 : 2 +>c : number +>3 : 3 +>d : (n: any) => any +>n : any +>n : any +>two : "a" | "d" + diff --git a/tests/cases/compiler/inferObjectTypeFromStringLiteralToKeyof.ts b/tests/cases/compiler/inferObjectTypeFromStringLiteralToKeyof.ts new file mode 100644 index 0000000000000..86acbf93a70e8 --- /dev/null +++ b/tests/cases/compiler/inferObjectTypeFromStringLiteralToKeyof.ts @@ -0,0 +1,3 @@ +declare function inference(target: T, name: keyof T): void; +declare var two: "a" | "d"; +inference({ a: 1, b: 2, c: 3, d(n) { return n } }, two); From abdfbaa067d0e2754878fed230fbcb56e6cd8658 Mon Sep 17 00:00:00 2001 From: Nathan Shively-Sanders Date: Tue, 17 Oct 2017 09:59:32 -0700 Subject: [PATCH 3/3] Fix lint --- src/compiler/checker.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/compiler/checker.ts b/src/compiler/checker.ts index 6f092825a7b70..5af71acb4b2c3 100644 --- a/src/compiler/checker.ts +++ b/src/compiler/checker.ts @@ -10664,7 +10664,7 @@ namespace ts { } members.set(name, literalProp); }); - const indexInfo = type.flags & TypeFlags.String ? createIndexInfo(emptyObjectType, /*isReadonly*/ false) : undefined + const indexInfo = type.flags & TypeFlags.String ? createIndexInfo(emptyObjectType, /*isReadonly*/ false) : undefined; return createAnonymousType(undefined, members, emptyArray, emptyArray, indexInfo, undefined); }