From f41ae547eb417b8721c92f9f88843ac95df57379 Mon Sep 17 00:00:00 2001 From: Wesley Wigham Date: Fri, 6 Jul 2018 15:00:23 -0700 Subject: [PATCH 1/2] Add transformation to indexing intersection of homomorphic mapepd type and generic parameter --- src/compiler/checker.ts | 35 ++++++++ ...ppedTypeHaveCorrectApparentType.errors.txt | 29 +++++++ ...bersOfMappedTypeHaveCorrectApparentType.js | 34 ++++++++ ...fMappedTypeHaveCorrectApparentType.symbols | 75 +++++++++++++++++ ...sOfMappedTypeHaveCorrectApparentType.types | 81 +++++++++++++++++++ ...bersOfMappedTypeHaveCorrectApparentType.ts | 23 ++++++ 6 files changed, 277 insertions(+) create mode 100644 tests/baselines/reference/intersectionMembersOfMappedTypeHaveCorrectApparentType.errors.txt create mode 100644 tests/baselines/reference/intersectionMembersOfMappedTypeHaveCorrectApparentType.js create mode 100644 tests/baselines/reference/intersectionMembersOfMappedTypeHaveCorrectApparentType.symbols create mode 100644 tests/baselines/reference/intersectionMembersOfMappedTypeHaveCorrectApparentType.types create mode 100644 tests/cases/compiler/intersectionMembersOfMappedTypeHaveCorrectApparentType.ts diff --git a/src/compiler/checker.ts b/src/compiler/checker.ts index 0eaffe07c02c9..be3fef76d60d7 100644 --- a/src/compiler/checker.ts +++ b/src/compiler/checker.ts @@ -8999,6 +8999,14 @@ namespace ts { return !!(getObjectFlags(type) & ObjectFlags.Mapped) && getTemplateTypeFromMappedType(type as MappedType) === neverType; } + function isMappedTypeOverKeyofGenericInSet(set: ReadonlyArray): (type: Type) => boolean { + return (type) => { + if (!(getObjectFlags(type) & ObjectFlags.Mapped)) return false; + const constraint = getConstraintTypeFromMappedType(type as MappedType); + return some(set, t => getIndexType(t) === constraint); + } + } + function getSimplifiedType(type: Type): Type { return type.flags & TypeFlags.IndexedAccess ? getSimplifiedIndexedAccessType(type) : type; } @@ -9042,6 +9050,33 @@ namespace ts { const nonNeverTypes = filter((objectType).types, t => !isMappedTypeToNever(t)); return type.simplified = getSimplifiedType(getIndexedAccessType(getIntersectionType(nonNeverTypes), type.indexType)); } + + // Given an indexed access type T[K], if T is an intersection containing a generic type and one or + // more mapped types which map over the keys of that generic, '(T & {[P in keyof T]: Foo})[K]', return a + // transformed type that moves the top-level intersection into the mapped type: '{[P in keyof T]: T[P] & Foo}[K]' + // If we don't do such a transformation now, due to how we reason over intersections, we will never come to the + // realization that the member we're trying to pluck out is influenced by both `T[P]` _and_ the mapped type template + const isInSet = isMappedTypeOverKeyofGenericInSet((objectType).types); + if (some((objectType).types, isInSet)) { + for (const t of (objectType).types) { + if (isInSet(t)) { + const constraint = getConstraintTypeFromMappedType(t as MappedType); + let matchIndex: number; + const match = forEach((objectType).types, (t, i) => getIndexType(t) === constraint ? (matchIndex = i, t) : undefined)!; + const indexedMatch = getIndexedAccessType(match, getTypeParameterFromMappedType(t as MappedType)); + const newTemaplate = getIntersectionType([indexedMatch, getTemplateTypeFromMappedType(t as MappedType)]); + const newMappedType = createObjectType(ObjectFlags.Mapped, t.symbol); + newMappedType.declaration = (t as MappedType).declaration; + newMappedType.templateType = newTemaplate; + newMappedType.typeParameter = (t as MappedType).typeParameter; + newMappedType.constraintType = (t as MappedType).constraintType; + const newSet = (objectType).types.slice(); + newSet.splice(matchIndex!, 1); // Remove the generic + newSet.splice(newSet.indexOf(t), 1, newMappedType); // Remove the original mapped type + return type.simplified = getSimplifiedType(getIndexedAccessType(getIntersectionType(newSet), type.indexType)); + } + } + } } // If the object type is a mapped type { [P in K]: E }, where K is generic, instantiate E using a mapper // that substitutes the index type for P. For example, for an index access { [P in K]: Box }[X], we diff --git a/tests/baselines/reference/intersectionMembersOfMappedTypeHaveCorrectApparentType.errors.txt b/tests/baselines/reference/intersectionMembersOfMappedTypeHaveCorrectApparentType.errors.txt new file mode 100644 index 0000000000000..c023380f81af5 --- /dev/null +++ b/tests/baselines/reference/intersectionMembersOfMappedTypeHaveCorrectApparentType.errors.txt @@ -0,0 +1,29 @@ +tests/cases/compiler/intersectionMembersOfMappedTypeHaveCorrectApparentType.ts(20,32): error TS2339: Property 'returnValue' does not exist on type 'Function'. + + +==== tests/cases/compiler/intersectionMembersOfMappedTypeHaveCorrectApparentType.ts (1 errors) ==== + interface Spy { + (...params: any[]): any; + + identity: string; + and: Function; + mostRecentCall: { args: any[]; }; + argsForCall: any[]; + } + + type SpyObj = T & { + [k in keyof T]: Spy; + } + + declare function createSpyObj( + name: string, names: Array): SpyObj; + + function mock(spyName: string, methodNames: Array): SpyObj { + const spyObj = createSpyObj(spyName, methodNames); + for (const methodName of methodNames) { + spyObj[methodName].and.returnValue(1); + ~~~~~~~~~~~ +!!! error TS2339: Property 'returnValue' does not exist on type 'Function'. + } + return spyObj; + } \ No newline at end of file diff --git a/tests/baselines/reference/intersectionMembersOfMappedTypeHaveCorrectApparentType.js b/tests/baselines/reference/intersectionMembersOfMappedTypeHaveCorrectApparentType.js new file mode 100644 index 0000000000000..36716b5cb3f88 --- /dev/null +++ b/tests/baselines/reference/intersectionMembersOfMappedTypeHaveCorrectApparentType.js @@ -0,0 +1,34 @@ +//// [intersectionMembersOfMappedTypeHaveCorrectApparentType.ts] +interface Spy { + (...params: any[]): any; + + identity: string; + and: Function; + mostRecentCall: { args: any[]; }; + argsForCall: any[]; +} + +type SpyObj = T & { + [k in keyof T]: Spy; +} + +declare function createSpyObj( + name: string, names: Array): SpyObj; + +function mock(spyName: string, methodNames: Array): SpyObj { + const spyObj = createSpyObj(spyName, methodNames); + for (const methodName of methodNames) { + spyObj[methodName].and.returnValue(1); + } + return spyObj; +} + +//// [intersectionMembersOfMappedTypeHaveCorrectApparentType.js] +function mock(spyName, methodNames) { + var spyObj = createSpyObj(spyName, methodNames); + for (var _i = 0, methodNames_1 = methodNames; _i < methodNames_1.length; _i++) { + var methodName = methodNames_1[_i]; + spyObj[methodName].and.returnValue(1); + } + return spyObj; +} diff --git a/tests/baselines/reference/intersectionMembersOfMappedTypeHaveCorrectApparentType.symbols b/tests/baselines/reference/intersectionMembersOfMappedTypeHaveCorrectApparentType.symbols new file mode 100644 index 0000000000000..13087de3c439a --- /dev/null +++ b/tests/baselines/reference/intersectionMembersOfMappedTypeHaveCorrectApparentType.symbols @@ -0,0 +1,75 @@ +=== tests/cases/compiler/intersectionMembersOfMappedTypeHaveCorrectApparentType.ts === +interface Spy { +>Spy : Symbol(Spy, Decl(intersectionMembersOfMappedTypeHaveCorrectApparentType.ts, 0, 0)) + + (...params: any[]): any; +>params : Symbol(params, Decl(intersectionMembersOfMappedTypeHaveCorrectApparentType.ts, 1, 5)) + + identity: string; +>identity : Symbol(Spy.identity, Decl(intersectionMembersOfMappedTypeHaveCorrectApparentType.ts, 1, 28)) + + and: Function; +>and : Symbol(Spy.and, Decl(intersectionMembersOfMappedTypeHaveCorrectApparentType.ts, 3, 21)) +>Function : Symbol(Function, Decl(lib.es5.d.ts, --, --), Decl(lib.es5.d.ts, --, --)) + + mostRecentCall: { args: any[]; }; +>mostRecentCall : Symbol(Spy.mostRecentCall, Decl(intersectionMembersOfMappedTypeHaveCorrectApparentType.ts, 4, 18)) +>args : Symbol(args, Decl(intersectionMembersOfMappedTypeHaveCorrectApparentType.ts, 5, 21)) + + argsForCall: any[]; +>argsForCall : Symbol(Spy.argsForCall, Decl(intersectionMembersOfMappedTypeHaveCorrectApparentType.ts, 5, 37)) +} + +type SpyObj = T & { +>SpyObj : Symbol(SpyObj, Decl(intersectionMembersOfMappedTypeHaveCorrectApparentType.ts, 7, 1)) +>T : Symbol(T, Decl(intersectionMembersOfMappedTypeHaveCorrectApparentType.ts, 9, 12)) +>T : Symbol(T, Decl(intersectionMembersOfMappedTypeHaveCorrectApparentType.ts, 9, 12)) + + [k in keyof T]: Spy; +>k : Symbol(k, Decl(intersectionMembersOfMappedTypeHaveCorrectApparentType.ts, 10, 5)) +>T : Symbol(T, Decl(intersectionMembersOfMappedTypeHaveCorrectApparentType.ts, 9, 12)) +>Spy : Symbol(Spy, Decl(intersectionMembersOfMappedTypeHaveCorrectApparentType.ts, 0, 0)) +} + +declare function createSpyObj( +>createSpyObj : Symbol(createSpyObj, Decl(intersectionMembersOfMappedTypeHaveCorrectApparentType.ts, 11, 1)) +>T : Symbol(T, Decl(intersectionMembersOfMappedTypeHaveCorrectApparentType.ts, 13, 30)) + + name: string, names: Array): SpyObj; +>name : Symbol(name, Decl(intersectionMembersOfMappedTypeHaveCorrectApparentType.ts, 13, 33)) +>names : Symbol(names, Decl(intersectionMembersOfMappedTypeHaveCorrectApparentType.ts, 14, 17)) +>Array : Symbol(Array, Decl(lib.es5.d.ts, --, --), Decl(lib.es5.d.ts, --, --)) +>T : Symbol(T, Decl(intersectionMembersOfMappedTypeHaveCorrectApparentType.ts, 13, 30)) +>SpyObj : Symbol(SpyObj, Decl(intersectionMembersOfMappedTypeHaveCorrectApparentType.ts, 7, 1)) +>T : Symbol(T, Decl(intersectionMembersOfMappedTypeHaveCorrectApparentType.ts, 13, 30)) + +function mock(spyName: string, methodNames: Array): SpyObj { +>mock : Symbol(mock, Decl(intersectionMembersOfMappedTypeHaveCorrectApparentType.ts, 14, 52)) +>T : Symbol(T, Decl(intersectionMembersOfMappedTypeHaveCorrectApparentType.ts, 16, 14)) +>spyName : Symbol(spyName, Decl(intersectionMembersOfMappedTypeHaveCorrectApparentType.ts, 16, 17)) +>methodNames : Symbol(methodNames, Decl(intersectionMembersOfMappedTypeHaveCorrectApparentType.ts, 16, 33)) +>Array : Symbol(Array, Decl(lib.es5.d.ts, --, --), Decl(lib.es5.d.ts, --, --)) +>T : Symbol(T, Decl(intersectionMembersOfMappedTypeHaveCorrectApparentType.ts, 16, 14)) +>SpyObj : Symbol(SpyObj, Decl(intersectionMembersOfMappedTypeHaveCorrectApparentType.ts, 7, 1)) +>T : Symbol(T, Decl(intersectionMembersOfMappedTypeHaveCorrectApparentType.ts, 16, 14)) + + const spyObj = createSpyObj(spyName, methodNames); +>spyObj : Symbol(spyObj, Decl(intersectionMembersOfMappedTypeHaveCorrectApparentType.ts, 17, 9)) +>createSpyObj : Symbol(createSpyObj, Decl(intersectionMembersOfMappedTypeHaveCorrectApparentType.ts, 11, 1)) +>T : Symbol(T, Decl(intersectionMembersOfMappedTypeHaveCorrectApparentType.ts, 16, 14)) +>spyName : Symbol(spyName, Decl(intersectionMembersOfMappedTypeHaveCorrectApparentType.ts, 16, 17)) +>methodNames : Symbol(methodNames, Decl(intersectionMembersOfMappedTypeHaveCorrectApparentType.ts, 16, 33)) + + for (const methodName of methodNames) { +>methodName : Symbol(methodName, Decl(intersectionMembersOfMappedTypeHaveCorrectApparentType.ts, 18, 14)) +>methodNames : Symbol(methodNames, Decl(intersectionMembersOfMappedTypeHaveCorrectApparentType.ts, 16, 33)) + + spyObj[methodName].and.returnValue(1); +>spyObj[methodName].and : Symbol(Spy.and, Decl(intersectionMembersOfMappedTypeHaveCorrectApparentType.ts, 3, 21)) +>spyObj : Symbol(spyObj, Decl(intersectionMembersOfMappedTypeHaveCorrectApparentType.ts, 17, 9)) +>methodName : Symbol(methodName, Decl(intersectionMembersOfMappedTypeHaveCorrectApparentType.ts, 18, 14)) +>and : Symbol(Spy.and, Decl(intersectionMembersOfMappedTypeHaveCorrectApparentType.ts, 3, 21)) + } + return spyObj; +>spyObj : Symbol(spyObj, Decl(intersectionMembersOfMappedTypeHaveCorrectApparentType.ts, 17, 9)) +} diff --git a/tests/baselines/reference/intersectionMembersOfMappedTypeHaveCorrectApparentType.types b/tests/baselines/reference/intersectionMembersOfMappedTypeHaveCorrectApparentType.types new file mode 100644 index 0000000000000..92e6117d635c2 --- /dev/null +++ b/tests/baselines/reference/intersectionMembersOfMappedTypeHaveCorrectApparentType.types @@ -0,0 +1,81 @@ +=== tests/cases/compiler/intersectionMembersOfMappedTypeHaveCorrectApparentType.ts === +interface Spy { +>Spy : Spy + + (...params: any[]): any; +>params : any[] + + identity: string; +>identity : string + + and: Function; +>and : Function +>Function : Function + + mostRecentCall: { args: any[]; }; +>mostRecentCall : { args: any[]; } +>args : any[] + + argsForCall: any[]; +>argsForCall : any[] +} + +type SpyObj = T & { +>SpyObj : SpyObj +>T : T +>T : T + + [k in keyof T]: Spy; +>k : k +>T : T +>Spy : Spy +} + +declare function createSpyObj( +>createSpyObj : (name: string, names: (keyof T)[]) => SpyObj +>T : T + + name: string, names: Array): SpyObj; +>name : string +>names : (keyof T)[] +>Array : T[] +>T : T +>SpyObj : SpyObj +>T : T + +function mock(spyName: string, methodNames: Array): SpyObj { +>mock : (spyName: string, methodNames: (keyof T)[]) => SpyObj +>T : T +>spyName : string +>methodNames : (keyof T)[] +>Array : T[] +>T : T +>SpyObj : SpyObj +>T : T + + const spyObj = createSpyObj(spyName, methodNames); +>spyObj : SpyObj +>createSpyObj(spyName, methodNames) : SpyObj +>createSpyObj : (name: string, names: (keyof T)[]) => SpyObj +>T : T +>spyName : string +>methodNames : (keyof T)[] + + for (const methodName of methodNames) { +>methodName : keyof T +>methodNames : (keyof T)[] + + spyObj[methodName].and.returnValue(1); +>spyObj[methodName].and.returnValue(1) : any +>spyObj[methodName].and.returnValue : any +>spyObj[methodName].and : Function +>spyObj[methodName] : SpyObj[keyof T] +>spyObj : SpyObj +>methodName : keyof T +>and : Function +>returnValue : any +>1 : 1 + } + return spyObj; +>spyObj : SpyObj +} diff --git a/tests/cases/compiler/intersectionMembersOfMappedTypeHaveCorrectApparentType.ts b/tests/cases/compiler/intersectionMembersOfMappedTypeHaveCorrectApparentType.ts new file mode 100644 index 0000000000000..68b53de98db46 --- /dev/null +++ b/tests/cases/compiler/intersectionMembersOfMappedTypeHaveCorrectApparentType.ts @@ -0,0 +1,23 @@ +interface Spy { + (...params: any[]): any; + + identity: string; + and: Function; + mostRecentCall: { args: any[]; }; + argsForCall: any[]; +} + +type SpyObj = T & { + [k in keyof T]: Spy; +} + +declare function createSpyObj( + name: string, names: Array): SpyObj; + +function mock(spyName: string, methodNames: Array): SpyObj { + const spyObj = createSpyObj(spyName, methodNames); + for (const methodName of methodNames) { + spyObj[methodName].and.returnValue(1); + } + return spyObj; +} \ No newline at end of file From 580068003d87a30a7d7f60ab7038851c830a2f3f Mon Sep 17 00:00:00 2001 From: Wesley Wigham Date: Fri, 6 Jul 2018 16:56:15 -0700 Subject: [PATCH 2/2] Fix lints --- src/compiler/checker.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/compiler/checker.ts b/src/compiler/checker.ts index be3fef76d60d7..9743e4ff741f6 100644 --- a/src/compiler/checker.ts +++ b/src/compiler/checker.ts @@ -9004,7 +9004,7 @@ namespace ts { if (!(getObjectFlags(type) & ObjectFlags.Mapped)) return false; const constraint = getConstraintTypeFromMappedType(type as MappedType); return some(set, t => getIndexType(t) === constraint); - } + }; } function getSimplifiedType(type: Type): Type { @@ -9061,7 +9061,7 @@ namespace ts { for (const t of (objectType).types) { if (isInSet(t)) { const constraint = getConstraintTypeFromMappedType(t as MappedType); - let matchIndex: number; + let matchIndex: number | undefined; const match = forEach((objectType).types, (t, i) => getIndexType(t) === constraint ? (matchIndex = i, t) : undefined)!; const indexedMatch = getIndexedAccessType(match, getTypeParameterFromMappedType(t as MappedType)); const newTemaplate = getIntersectionType([indexedMatch, getTemplateTypeFromMappedType(t as MappedType)]);