From 12b80f3183cd97aacf448c29d609aa4b56be260c Mon Sep 17 00:00:00 2001 From: Nathan Shively-Sanders <293473+sandersn@users.noreply.github.com> Date: Thu, 11 Jan 2018 16:26:38 -0800 Subject: [PATCH 1/5] Keep string index from intersections in base constraint of index type --- src/compiler/checker.ts | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/compiler/checker.ts b/src/compiler/checker.ts index d402f344d60eb..a32104c053502 100644 --- a/src/compiler/checker.ts +++ b/src/compiler/checker.ts @@ -6468,7 +6468,7 @@ namespace ts { } function getConstraintOfIndexedAccess(type: IndexedAccessType) { - const transformed = getSimplifiedIndexedAccessType(type); + const transformed = getSubstitutedIndexedMappedType(type); if (transformed) { return transformed; } @@ -8383,6 +8383,11 @@ namespace ts { getIntersectionType(stringIndexTypes) ]); } + return getSubstitutedIndexedMappedType(type); + } + + function getSubstitutedIndexedMappedType(type: IndexedAccessType): Type { + const objectType = type.objectType; // 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 // construct the type Box. From baf31ec52e500bd646090f62fcb4b6112bb09645 Mon Sep 17 00:00:00 2001 From: Nathan Shively-Sanders <293473+sandersn@users.noreply.github.com> Date: Thu, 11 Jan 2018 16:30:05 -0800 Subject: [PATCH 2/5] Test Diff and Omit --- .../indexedAccessRetainsIndexSignature.js | 10 ++++++ ...indexedAccessRetainsIndexSignature.symbols | 33 +++++++++++++++++++ .../indexedAccessRetainsIndexSignature.types | 33 +++++++++++++++++++ .../indexedAccessRetainsIndexSignature.ts | 6 ++++ 4 files changed, 82 insertions(+) create mode 100644 tests/baselines/reference/indexedAccessRetainsIndexSignature.js create mode 100644 tests/baselines/reference/indexedAccessRetainsIndexSignature.symbols create mode 100644 tests/baselines/reference/indexedAccessRetainsIndexSignature.types create mode 100644 tests/cases/compiler/indexedAccessRetainsIndexSignature.ts diff --git a/tests/baselines/reference/indexedAccessRetainsIndexSignature.js b/tests/baselines/reference/indexedAccessRetainsIndexSignature.js new file mode 100644 index 0000000000000..6c33317f156b2 --- /dev/null +++ b/tests/baselines/reference/indexedAccessRetainsIndexSignature.js @@ -0,0 +1,10 @@ +//// [indexedAccessRetainsIndexSignature.ts] +type Diff = + ({ [P in T]: P } & { [P in U]: never } & { [x: string]: never })[T] +type Omit = Pick> + + +type O = Omit<{ a: number, b: string }, 'a'> + + +//// [indexedAccessRetainsIndexSignature.js] diff --git a/tests/baselines/reference/indexedAccessRetainsIndexSignature.symbols b/tests/baselines/reference/indexedAccessRetainsIndexSignature.symbols new file mode 100644 index 0000000000000..908d74c945200 --- /dev/null +++ b/tests/baselines/reference/indexedAccessRetainsIndexSignature.symbols @@ -0,0 +1,33 @@ +=== tests/cases/compiler/indexedAccessRetainsIndexSignature.ts === +type Diff = +>Diff : Symbol(Diff, Decl(indexedAccessRetainsIndexSignature.ts, 0, 0)) +>T : Symbol(T, Decl(indexedAccessRetainsIndexSignature.ts, 0, 10)) +>U : Symbol(U, Decl(indexedAccessRetainsIndexSignature.ts, 0, 27)) + + ({ [P in T]: P } & { [P in U]: never } & { [x: string]: never })[T] +>P : Symbol(P, Decl(indexedAccessRetainsIndexSignature.ts, 1, 8)) +>T : Symbol(T, Decl(indexedAccessRetainsIndexSignature.ts, 0, 10)) +>P : Symbol(P, Decl(indexedAccessRetainsIndexSignature.ts, 1, 8)) +>P : Symbol(P, Decl(indexedAccessRetainsIndexSignature.ts, 1, 26)) +>U : Symbol(U, Decl(indexedAccessRetainsIndexSignature.ts, 0, 27)) +>x : Symbol(x, Decl(indexedAccessRetainsIndexSignature.ts, 1, 48)) +>T : Symbol(T, Decl(indexedAccessRetainsIndexSignature.ts, 0, 10)) + +type Omit = Pick> +>Omit : Symbol(Omit, Decl(indexedAccessRetainsIndexSignature.ts, 1, 71)) +>U : Symbol(U, Decl(indexedAccessRetainsIndexSignature.ts, 2, 10)) +>K : Symbol(K, Decl(indexedAccessRetainsIndexSignature.ts, 2, 12)) +>U : Symbol(U, Decl(indexedAccessRetainsIndexSignature.ts, 2, 10)) +>Pick : Symbol(Pick, Decl(lib.d.ts, --, --)) +>U : Symbol(U, Decl(indexedAccessRetainsIndexSignature.ts, 2, 10)) +>Diff : Symbol(Diff, Decl(indexedAccessRetainsIndexSignature.ts, 0, 0)) +>U : Symbol(U, Decl(indexedAccessRetainsIndexSignature.ts, 2, 10)) +>K : Symbol(K, Decl(indexedAccessRetainsIndexSignature.ts, 2, 12)) + + +type O = Omit<{ a: number, b: string }, 'a'> +>O : Symbol(O, Decl(indexedAccessRetainsIndexSignature.ts, 2, 59)) +>Omit : Symbol(Omit, Decl(indexedAccessRetainsIndexSignature.ts, 1, 71)) +>a : Symbol(a, Decl(indexedAccessRetainsIndexSignature.ts, 5, 15)) +>b : Symbol(b, Decl(indexedAccessRetainsIndexSignature.ts, 5, 26)) + diff --git a/tests/baselines/reference/indexedAccessRetainsIndexSignature.types b/tests/baselines/reference/indexedAccessRetainsIndexSignature.types new file mode 100644 index 0000000000000..8ef6cb62559cc --- /dev/null +++ b/tests/baselines/reference/indexedAccessRetainsIndexSignature.types @@ -0,0 +1,33 @@ +=== tests/cases/compiler/indexedAccessRetainsIndexSignature.ts === +type Diff = +>Diff : ({ [P in T]: P; } & { [P in U]: never; } & { [x: string]: never; })[T] +>T : T +>U : U + + ({ [P in T]: P } & { [P in U]: never } & { [x: string]: never })[T] +>P : P +>T : T +>P : P +>P : P +>U : U +>x : string +>T : T + +type Omit = Pick> +>Omit : Pick +>U : U +>K : K +>U : U +>Pick : Pick +>U : U +>Diff : ({ [P in T]: P; } & { [P in U]: never; } & { [x: string]: never; })[T] +>U : U +>K : K + + +type O = Omit<{ a: number, b: string }, 'a'> +>O : Pick<{ a: number; b: string; }, "b"> +>Omit : Pick +>a : number +>b : string + diff --git a/tests/cases/compiler/indexedAccessRetainsIndexSignature.ts b/tests/cases/compiler/indexedAccessRetainsIndexSignature.ts new file mode 100644 index 0000000000000..ff38b2d909056 --- /dev/null +++ b/tests/cases/compiler/indexedAccessRetainsIndexSignature.ts @@ -0,0 +1,6 @@ +type Diff = + ({ [P in T]: P } & { [P in U]: never } & { [x: string]: never })[T] +type Omit = Pick> + + +type O = Omit<{ a: number, b: string }, 'a'> From d74820d51997d6a84edd9acfe2916e0090240f5b Mon Sep 17 00:00:00 2001 From: Nathan Shively-Sanders <293473+sandersn@users.noreply.github.com> Date: Fri, 12 Jan 2018 14:43:31 -0800 Subject: [PATCH 3/5] Remove mapped types to never from intersections when transforming an indexed access type in order to get its constraint. --- src/compiler/checker.ts | 25 ++++++++++--- .../indexedAccessRetainsIndexSignature.js | 5 +++ ...indexedAccessRetainsIndexSignature.symbols | 35 +++++++++++++++++-- .../indexedAccessRetainsIndexSignature.types | 31 ++++++++++++++++ .../indexedAccessRetainsIndexSignature.ts | 4 +++ 5 files changed, 92 insertions(+), 8 deletions(-) diff --git a/src/compiler/checker.ts b/src/compiler/checker.ts index a32104c053502..1a5cc357c78bb 100644 --- a/src/compiler/checker.ts +++ b/src/compiler/checker.ts @@ -6468,7 +6468,7 @@ namespace ts { } function getConstraintOfIndexedAccess(type: IndexedAccessType) { - const transformed = getSubstitutedIndexedMappedType(type); + const transformed = getSimplifiedIndexedAccessType(type); if (transformed) { return transformed; } @@ -8359,6 +8359,10 @@ namespace ts { return false; } + function isMappedTypeToNever(type: Type) { + return getObjectFlags(type) & ObjectFlags.Mapped && getTemplateTypeFromMappedType(type as MappedType) === neverType; + } + // Transform an indexed access to a simpler form, if possible. Return the simpler form, or return // undefined if no transformation is possible. function getSimplifiedIndexedAccessType(type: IndexedAccessType): Type { @@ -8383,11 +8387,22 @@ namespace ts { getIntersectionType(stringIndexTypes) ]); } - return getSubstitutedIndexedMappedType(type); - } + // Given an indexed access type T[K], if T is an intersection containing one or more generic types and one or + // more mapped types with a template type `never`, '(U & V & { [P in T]: never })[K]', return a + // transformed type that removes the never-mapped type: '(U & V)[K]'. This mirrors what would happen + // eventually anyway, but it easier to reason about. + if (objectType.flags & TypeFlags.Intersection && isGenericObjectType(objectType) && some((objectType).types, isMappedTypeToNever)) { + let nonNeverTypes: Type[]; + for (const t of (objectType).types) { + if (!isMappedTypeToNever(t)) { + (nonNeverTypes || (nonNeverTypes = [])).push(t); + } + } + if (nonNeverTypes) { + return getIndexedAccessType(getIntersectionType(nonNeverTypes), type.indexType); + } + } - function getSubstitutedIndexedMappedType(type: IndexedAccessType): Type { - const objectType = type.objectType; // 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 // construct the type Box. diff --git a/tests/baselines/reference/indexedAccessRetainsIndexSignature.js b/tests/baselines/reference/indexedAccessRetainsIndexSignature.js index 6c33317f156b2..1b77b95527bec 100644 --- a/tests/baselines/reference/indexedAccessRetainsIndexSignature.js +++ b/tests/baselines/reference/indexedAccessRetainsIndexSignature.js @@ -2,9 +2,14 @@ type Diff = ({ [P in T]: P } & { [P in U]: never } & { [x: string]: never })[T] type Omit = Pick> +type Omit1 = Pick>; +// is in fact an equivalent of +type Omit2 = {[P in Diff]: T[P]}; type O = Omit<{ a: number, b: string }, 'a'> +const o: O = { b: '' } //// [indexedAccessRetainsIndexSignature.js] +var o = { b: '' }; diff --git a/tests/baselines/reference/indexedAccessRetainsIndexSignature.symbols b/tests/baselines/reference/indexedAccessRetainsIndexSignature.symbols index 908d74c945200..9858f6fc16209 100644 --- a/tests/baselines/reference/indexedAccessRetainsIndexSignature.symbols +++ b/tests/baselines/reference/indexedAccessRetainsIndexSignature.symbols @@ -24,10 +24,39 @@ type Omit = Pick> >U : Symbol(U, Decl(indexedAccessRetainsIndexSignature.ts, 2, 10)) >K : Symbol(K, Decl(indexedAccessRetainsIndexSignature.ts, 2, 12)) +type Omit1 = Pick>; +>Omit1 : Symbol(Omit1, Decl(indexedAccessRetainsIndexSignature.ts, 2, 59)) +>U : Symbol(U, Decl(indexedAccessRetainsIndexSignature.ts, 3, 11)) +>K : Symbol(K, Decl(indexedAccessRetainsIndexSignature.ts, 3, 13)) +>U : Symbol(U, Decl(indexedAccessRetainsIndexSignature.ts, 3, 11)) +>Pick : Symbol(Pick, Decl(lib.d.ts, --, --)) +>U : Symbol(U, Decl(indexedAccessRetainsIndexSignature.ts, 3, 11)) +>Diff : Symbol(Diff, Decl(indexedAccessRetainsIndexSignature.ts, 0, 0)) +>U : Symbol(U, Decl(indexedAccessRetainsIndexSignature.ts, 3, 11)) +>K : Symbol(K, Decl(indexedAccessRetainsIndexSignature.ts, 3, 13)) + +// is in fact an equivalent of + +type Omit2 = {[P in Diff]: T[P]}; +>Omit2 : Symbol(Omit2, Decl(indexedAccessRetainsIndexSignature.ts, 3, 61)) +>T : Symbol(T, Decl(indexedAccessRetainsIndexSignature.ts, 6, 11)) +>K : Symbol(K, Decl(indexedAccessRetainsIndexSignature.ts, 6, 13)) +>T : Symbol(T, Decl(indexedAccessRetainsIndexSignature.ts, 6, 11)) +>P : Symbol(P, Decl(indexedAccessRetainsIndexSignature.ts, 6, 37)) +>Diff : Symbol(Diff, Decl(indexedAccessRetainsIndexSignature.ts, 0, 0)) +>T : Symbol(T, Decl(indexedAccessRetainsIndexSignature.ts, 6, 11)) +>K : Symbol(K, Decl(indexedAccessRetainsIndexSignature.ts, 6, 13)) +>T : Symbol(T, Decl(indexedAccessRetainsIndexSignature.ts, 6, 11)) +>P : Symbol(P, Decl(indexedAccessRetainsIndexSignature.ts, 6, 37)) type O = Omit<{ a: number, b: string }, 'a'> ->O : Symbol(O, Decl(indexedAccessRetainsIndexSignature.ts, 2, 59)) +>O : Symbol(O, Decl(indexedAccessRetainsIndexSignature.ts, 6, 67)) >Omit : Symbol(Omit, Decl(indexedAccessRetainsIndexSignature.ts, 1, 71)) ->a : Symbol(a, Decl(indexedAccessRetainsIndexSignature.ts, 5, 15)) ->b : Symbol(b, Decl(indexedAccessRetainsIndexSignature.ts, 5, 26)) +>a : Symbol(a, Decl(indexedAccessRetainsIndexSignature.ts, 8, 15)) +>b : Symbol(b, Decl(indexedAccessRetainsIndexSignature.ts, 8, 26)) + +const o: O = { b: '' } +>o : Symbol(o, Decl(indexedAccessRetainsIndexSignature.ts, 9, 5)) +>O : Symbol(O, Decl(indexedAccessRetainsIndexSignature.ts, 6, 67)) +>b : Symbol(b, Decl(indexedAccessRetainsIndexSignature.ts, 9, 14)) diff --git a/tests/baselines/reference/indexedAccessRetainsIndexSignature.types b/tests/baselines/reference/indexedAccessRetainsIndexSignature.types index 8ef6cb62559cc..3e3f52f7ca6e5 100644 --- a/tests/baselines/reference/indexedAccessRetainsIndexSignature.types +++ b/tests/baselines/reference/indexedAccessRetainsIndexSignature.types @@ -24,6 +24,30 @@ type Omit = Pick> >U : U >K : K +type Omit1 = Pick>; +>Omit1 : Pick +>U : U +>K : K +>U : U +>Pick : Pick +>U : U +>Diff : ({ [P in T]: P; } & { [P in U]: never; } & { [x: string]: never; })[T] +>U : U +>K : K + +// is in fact an equivalent of + +type Omit2 = {[P in Diff]: T[P]}; +>Omit2 : Omit2 +>T : T +>K : K +>T : T +>P : P +>Diff : ({ [P in T]: P; } & { [P in U]: never; } & { [x: string]: never; })[T] +>T : T +>K : K +>T : T +>P : P type O = Omit<{ a: number, b: string }, 'a'> >O : Pick<{ a: number; b: string; }, "b"> @@ -31,3 +55,10 @@ type O = Omit<{ a: number, b: string }, 'a'> >a : number >b : string +const o: O = { b: '' } +>o : Pick<{ a: number; b: string; }, "b"> +>O : Pick<{ a: number; b: string; }, "b"> +>{ b: '' } : { b: string; } +>b : string +>'' : "" + diff --git a/tests/cases/compiler/indexedAccessRetainsIndexSignature.ts b/tests/cases/compiler/indexedAccessRetainsIndexSignature.ts index ff38b2d909056..d23cbe87ddf87 100644 --- a/tests/cases/compiler/indexedAccessRetainsIndexSignature.ts +++ b/tests/cases/compiler/indexedAccessRetainsIndexSignature.ts @@ -1,6 +1,10 @@ type Diff = ({ [P in T]: P } & { [P in U]: never } & { [x: string]: never })[T] type Omit = Pick> +type Omit1 = Pick>; +// is in fact an equivalent of +type Omit2 = {[P in Diff]: T[P]}; type O = Omit<{ a: number, b: string }, 'a'> +const o: O = { b: '' } From f1622f0dc6b75a727ce8f0c2e2ef05a33dd31bbd Mon Sep 17 00:00:00 2001 From: Nathan Shively-Sanders <293473+sandersn@users.noreply.github.com> Date: Fri, 12 Jan 2018 14:56:50 -0800 Subject: [PATCH 4/5] Use filter instead of unnecessary laziness --- src/compiler/checker.ts | 13 +++---------- 1 file changed, 3 insertions(+), 10 deletions(-) diff --git a/src/compiler/checker.ts b/src/compiler/checker.ts index 1a5cc357c78bb..811fe868f9645 100644 --- a/src/compiler/checker.ts +++ b/src/compiler/checker.ts @@ -8389,18 +8389,11 @@ namespace ts { } // Given an indexed access type T[K], if T is an intersection containing one or more generic types and one or // more mapped types with a template type `never`, '(U & V & { [P in T]: never })[K]', return a - // transformed type that removes the never-mapped type: '(U & V)[K]'. This mirrors what would happen + // transformed type that removes the never-mapped type: '(U & V)[K]'. This mirrors what would happen // eventually anyway, but it easier to reason about. if (objectType.flags & TypeFlags.Intersection && isGenericObjectType(objectType) && some((objectType).types, isMappedTypeToNever)) { - let nonNeverTypes: Type[]; - for (const t of (objectType).types) { - if (!isMappedTypeToNever(t)) { - (nonNeverTypes || (nonNeverTypes = [])).push(t); - } - } - if (nonNeverTypes) { - return getIndexedAccessType(getIntersectionType(nonNeverTypes), type.indexType); - } + const nonNeverTypes = filter((objectType).types, t => !isMappedTypeToNever(t)); + return getIndexedAccessType(getIntersectionType(nonNeverTypes), type.indexType); } // If the object type is a mapped type { [P in K]: E }, where K is generic, instantiate E using a mapper From fd1bb9bde21f79bc90325990129f45dc3b0185a0 Mon Sep 17 00:00:00 2001 From: Nathan Shively-Sanders <293473+sandersn@users.noreply.github.com> Date: Fri, 12 Jan 2018 15:07:46 -0800 Subject: [PATCH 5/5] Group intersection code in getSimplifiedIndexedAccessType --- src/compiler/checker.ts | 52 +++++++++++++++++++++-------------------- 1 file changed, 27 insertions(+), 25 deletions(-) diff --git a/src/compiler/checker.ts b/src/compiler/checker.ts index 811fe868f9645..5e26b37b61ae0 100644 --- a/src/compiler/checker.ts +++ b/src/compiler/checker.ts @@ -8367,33 +8367,35 @@ namespace ts { // undefined if no transformation is possible. function getSimplifiedIndexedAccessType(type: IndexedAccessType): Type { const objectType = type.objectType; - // Given an indexed access type T[K], if T is an intersection containing one or more generic types and one or - // more object types with only a string index signature, e.g. '(U & V & { [x: string]: D })[K]', return a - // transformed type of the form '(U & V)[K] | D'. This allows us to properly reason about higher order indexed - // access types with default property values as expressed by D. - if (objectType.flags & TypeFlags.Intersection && isGenericObjectType(objectType) && some((objectType).types, isStringIndexOnlyType)) { - const regularTypes: Type[] = []; - const stringIndexTypes: Type[] = []; - for (const t of (objectType).types) { - if (isStringIndexOnlyType(t)) { - stringIndexTypes.push(getIndexTypeOfType(t, IndexKind.String)); - } - else { - regularTypes.push(t); + if (objectType.flags & TypeFlags.Intersection && isGenericObjectType(objectType)) { + // Given an indexed access type T[K], if T is an intersection containing one or more generic types and one or + // more object types with only a string index signature, e.g. '(U & V & { [x: string]: D })[K]', return a + // transformed type of the form '(U & V)[K] | D'. This allows us to properly reason about higher order indexed + // access types with default property values as expressed by D. + if (some((objectType).types, isStringIndexOnlyType)) { + const regularTypes: Type[] = []; + const stringIndexTypes: Type[] = []; + for (const t of (objectType).types) { + if (isStringIndexOnlyType(t)) { + stringIndexTypes.push(getIndexTypeOfType(t, IndexKind.String)); + } + else { + regularTypes.push(t); + } } + return getUnionType([ + getIndexedAccessType(getIntersectionType(regularTypes), type.indexType), + getIntersectionType(stringIndexTypes) + ]); + } + // Given an indexed access type T[K], if T is an intersection containing one or more generic types and one or + // more mapped types with a template type `never`, '(U & V & { [P in T]: never })[K]', return a + // transformed type that removes the never-mapped type: '(U & V)[K]'. This mirrors what would happen + // eventually anyway, but it easier to reason about. + if (some((objectType).types, isMappedTypeToNever)) { + const nonNeverTypes = filter((objectType).types, t => !isMappedTypeToNever(t)); + return getIndexedAccessType(getIntersectionType(nonNeverTypes), type.indexType); } - return getUnionType([ - getIndexedAccessType(getIntersectionType(regularTypes), type.indexType), - getIntersectionType(stringIndexTypes) - ]); - } - // Given an indexed access type T[K], if T is an intersection containing one or more generic types and one or - // more mapped types with a template type `never`, '(U & V & { [P in T]: never })[K]', return a - // transformed type that removes the never-mapped type: '(U & V)[K]'. This mirrors what would happen - // eventually anyway, but it easier to reason about. - if (objectType.flags & TypeFlags.Intersection && isGenericObjectType(objectType) && some((objectType).types, isMappedTypeToNever)) { - const nonNeverTypes = filter((objectType).types, t => !isMappedTypeToNever(t)); - return getIndexedAccessType(getIntersectionType(nonNeverTypes), type.indexType); } // If the object type is a mapped type { [P in K]: E }, where K is generic, instantiate E using a mapper