diff --git a/src/compiler/checker.ts b/src/compiler/checker.ts index a53c1841b6ef4..d3995362a08a1 100644 --- a/src/compiler/checker.ts +++ b/src/compiler/checker.ts @@ -15238,20 +15238,24 @@ namespace ts { * type parameters in the union with a special never type that is treated as a literal in `getReducedType`, we can cause the `getReducedType` logic * to reduce the resulting type if possible (since only intersections with conflicting literal-typed properties are reducible). */ - function isPossiblyReducibleByInstantiation(type: UnionType): boolean { - return some(type.types, t => { - const uniqueFilled = getUniqueLiteralFilledInstantiation(t); - return getReducedType(uniqueFilled) !== uniqueFilled; - }); + function isPossiblyReducibleByInstantiation(type: Type): boolean { + const uniqueFilled = getUniqueLiteralFilledInstantiation(type); + return getReducedType(uniqueFilled) !== uniqueFilled; + } + + function shouldDeferIndexType(type: Type) { + return !!(type.flags & TypeFlags.InstantiableNonPrimitive || + isGenericTupleType(type) || + isGenericMappedType(type) && !hasDistributiveNameType(type) || + type.flags & TypeFlags.Union && some((type as UnionType).types, isPossiblyReducibleByInstantiation) || + type.flags & TypeFlags.Intersection && maybeTypeOfKind(type, TypeFlags.Instantiable) && some((type as IntersectionType).types, isEmptyAnonymousObjectType)); } function getIndexType(type: Type, stringsOnly = keyofStringsOnly, noIndexSignatures?: boolean): Type { type = getReducedType(type); - return type.flags & TypeFlags.Union ? isPossiblyReducibleByInstantiation(type as UnionType) - ? getIndexTypeForGenericType(type as InstantiableType | UnionOrIntersectionType, stringsOnly) - : getIntersectionType(map((type as UnionType).types, t => getIndexType(t, stringsOnly, noIndexSignatures))) : + return shouldDeferIndexType(type) ? getIndexTypeForGenericType(type as InstantiableType | UnionOrIntersectionType, stringsOnly) : + type.flags & TypeFlags.Union ? getIntersectionType(map((type as UnionType).types, t => getIndexType(t, stringsOnly, noIndexSignatures))) : type.flags & TypeFlags.Intersection ? getUnionType(map((type as IntersectionType).types, t => getIndexType(t, stringsOnly, noIndexSignatures))) : - type.flags & TypeFlags.InstantiableNonPrimitive || isGenericTupleType(type) || isGenericMappedType(type) && !hasDistributiveNameType(type) ? getIndexTypeForGenericType(type as InstantiableType | UnionOrIntersectionType, stringsOnly) : getObjectFlags(type) & ObjectFlags.Mapped ? getIndexTypeForMappedType(type as MappedType, stringsOnly, noIndexSignatures) : type === wildcardType ? wildcardType : type.flags & TypeFlags.Unknown ? neverType : diff --git a/tests/baselines/reference/mappedTypes4.js b/tests/baselines/reference/mappedTypes4.js index 6237476906a75..f856fbcdf153c 100644 --- a/tests/baselines/reference/mappedTypes4.js +++ b/tests/baselines/reference/mappedTypes4.js @@ -63,7 +63,7 @@ var x1: DeepReadonlyFoo; type Z = { a: number }; type Clone = { - [P in keyof (T & {})]: T[P]; + [P in keyof (T & {})]: (T & {})[P]; }; type M = Clone; // M should be { a: number } @@ -144,7 +144,7 @@ declare type Z = { a: number; }; declare type Clone = { - [P in keyof (T & {})]: T[P]; + [P in keyof (T & {})]: (T & {})[P]; }; declare type M = Clone; declare var z1: Z; diff --git a/tests/baselines/reference/mappedTypes4.symbols b/tests/baselines/reference/mappedTypes4.symbols index 487f91c214e7b..e4c6e6872b3c8 100644 --- a/tests/baselines/reference/mappedTypes4.symbols +++ b/tests/baselines/reference/mappedTypes4.symbols @@ -205,7 +205,7 @@ type Clone = { >Clone : Symbol(Clone, Decl(mappedTypes4.ts, 62, 23)) >T : Symbol(T, Decl(mappedTypes4.ts, 63, 11)) - [P in keyof (T & {})]: T[P]; + [P in keyof (T & {})]: (T & {})[P]; >P : Symbol(P, Decl(mappedTypes4.ts, 64, 3)) >T : Symbol(T, Decl(mappedTypes4.ts, 63, 11)) >T : Symbol(T, Decl(mappedTypes4.ts, 63, 11)) diff --git a/tests/baselines/reference/mappedTypes4.types b/tests/baselines/reference/mappedTypes4.types index b51aefa9cce98..81e5b163d4661 100644 --- a/tests/baselines/reference/mappedTypes4.types +++ b/tests/baselines/reference/mappedTypes4.types @@ -160,10 +160,10 @@ type Z = { a: number }; type Clone = { >Clone : Clone - [P in keyof (T & {})]: T[P]; + [P in keyof (T & {})]: (T & {})[P]; }; type M = Clone; // M should be { a: number } ->M : Clone +>M : { a: number; } var z1: Z; >z1 : Z diff --git a/tests/baselines/reference/unknownControlFlow.errors.txt b/tests/baselines/reference/unknownControlFlow.errors.txt index 65108ba22011f..7c6db9cbd2cae 100644 --- a/tests/baselines/reference/unknownControlFlow.errors.txt +++ b/tests/baselines/reference/unknownControlFlow.errors.txt @@ -1,7 +1,11 @@ tests/cases/conformance/types/unknown/unknownControlFlow.ts(18,9): error TS2322: Type 'unknown' is not assignable to type '{}'. +tests/cases/conformance/types/unknown/unknownControlFlow.ts(283,5): error TS2536: Type 'keyof (T & {})' cannot be used to index type 'T'. +tests/cases/conformance/types/unknown/unknownControlFlow.ts(290,11): error TS2345: Argument of type 'string' is not assignable to parameter of type 'never'. +tests/cases/conformance/types/unknown/unknownControlFlow.ts(291,5): error TS2345: Argument of type 'null' is not assignable to parameter of type 'never'. +tests/cases/conformance/types/unknown/unknownControlFlow.ts(293,5): error TS2345: Argument of type 'null' is not assignable to parameter of type 'never'. -==== tests/cases/conformance/types/unknown/unknownControlFlow.ts (1 errors) ==== +==== tests/cases/conformance/types/unknown/unknownControlFlow.ts (5 errors) ==== type T01 = {} & string; // string type T02 = {} & 'a'; // 'a' type T03 = {} & object; // object @@ -272,4 +276,44 @@ tests/cases/conformance/types/unknown/unknownControlFlow.ts(18,9): error TS2322: y; } } + + // We allow an unconstrained object of a generic type `T` to be indexed by a key of type `keyof T` + // without a check that the object is non-undefined and non-null. This is safe because `keyof T` + // is `never` (meaning no possible keys) for any `T` that includes `undefined` or `null`. + + function ff1(t: T, k: keyof T) { + t[k]; + } + + function ff2(t: T & {}, k: keyof T) { + t[k]; + } + + function ff3(t: T, k: keyof (T & {})) { + t[k]; // Error + ~~~~ +!!! error TS2536: Type 'keyof (T & {})' cannot be used to index type 'T'. + } + + function ff4(t: T & {}, k: keyof (T & {})) { + t[k]; + } + + ff1(null, 'foo'); // Error + ~~~~~ +!!! error TS2345: Argument of type 'string' is not assignable to parameter of type 'never'. + ff2(null, 'foo'); // Error + ~~~~ +!!! error TS2345: Argument of type 'null' is not assignable to parameter of type 'never'. + ff3(null, 'foo'); + ff4(null, 'foo'); // Error + ~~~~ +!!! error TS2345: Argument of type 'null' is not assignable to parameter of type 'never'. + + // Repro from #49681 + + type Foo = { [key: string]: unknown }; + type NullableFoo = Foo | undefined; + + type Bar = NonNullable[string]; \ No newline at end of file diff --git a/tests/baselines/reference/unknownControlFlow.js b/tests/baselines/reference/unknownControlFlow.js index d665377767cca..b924cf83ffb83 100644 --- a/tests/baselines/reference/unknownControlFlow.js +++ b/tests/baselines/reference/unknownControlFlow.js @@ -267,6 +267,38 @@ function foo(x: T | null) { y; } } + +// We allow an unconstrained object of a generic type `T` to be indexed by a key of type `keyof T` +// without a check that the object is non-undefined and non-null. This is safe because `keyof T` +// is `never` (meaning no possible keys) for any `T` that includes `undefined` or `null`. + +function ff1(t: T, k: keyof T) { + t[k]; +} + +function ff2(t: T & {}, k: keyof T) { + t[k]; +} + +function ff3(t: T, k: keyof (T & {})) { + t[k]; // Error +} + +function ff4(t: T & {}, k: keyof (T & {})) { + t[k]; +} + +ff1(null, 'foo'); // Error +ff2(null, 'foo'); // Error +ff3(null, 'foo'); +ff4(null, 'foo'); // Error + +// Repro from #49681 + +type Foo = { [key: string]: unknown }; +type NullableFoo = Foo | undefined; + +type Bar = NonNullable[string]; //// [unknownControlFlow.js] @@ -501,6 +533,25 @@ function foo(x) { y; } } +// We allow an unconstrained object of a generic type `T` to be indexed by a key of type `keyof T` +// without a check that the object is non-undefined and non-null. This is safe because `keyof T` +// is `never` (meaning no possible keys) for any `T` that includes `undefined` or `null`. +function ff1(t, k) { + t[k]; +} +function ff2(t, k) { + t[k]; +} +function ff3(t, k) { + t[k]; // Error +} +function ff4(t, k) { + t[k]; +} +ff1(null, 'foo'); // Error +ff2(null, 'foo'); // Error +ff3(null, 'foo'); +ff4(null, 'foo'); // Error //// [unknownControlFlow.d.ts] @@ -541,3 +592,12 @@ declare type QQ = NonNullable>>; declare function f41(a: T): void; declare function deepEquals(a: T, b: T): boolean; declare function foo(x: T | null): void; +declare function ff1(t: T, k: keyof T): void; +declare function ff2(t: T & {}, k: keyof T): void; +declare function ff3(t: T, k: keyof (T & {})): void; +declare function ff4(t: T & {}, k: keyof (T & {})): void; +declare type Foo = { + [key: string]: unknown; +}; +declare type NullableFoo = Foo | undefined; +declare type Bar = NonNullable[string]; diff --git a/tests/baselines/reference/unknownControlFlow.symbols b/tests/baselines/reference/unknownControlFlow.symbols index 176df97b14518..47b6d4411a530 100644 --- a/tests/baselines/reference/unknownControlFlow.symbols +++ b/tests/baselines/reference/unknownControlFlow.symbols @@ -636,3 +636,88 @@ function foo(x: T | null) { } } +// We allow an unconstrained object of a generic type `T` to be indexed by a key of type `keyof T` +// without a check that the object is non-undefined and non-null. This is safe because `keyof T` +// is `never` (meaning no possible keys) for any `T` that includes `undefined` or `null`. + +function ff1(t: T, k: keyof T) { +>ff1 : Symbol(ff1, Decl(unknownControlFlow.ts, 267, 1)) +>T : Symbol(T, Decl(unknownControlFlow.ts, 273, 13)) +>t : Symbol(t, Decl(unknownControlFlow.ts, 273, 16)) +>T : Symbol(T, Decl(unknownControlFlow.ts, 273, 13)) +>k : Symbol(k, Decl(unknownControlFlow.ts, 273, 21)) +>T : Symbol(T, Decl(unknownControlFlow.ts, 273, 13)) + + t[k]; +>t : Symbol(t, Decl(unknownControlFlow.ts, 273, 16)) +>k : Symbol(k, Decl(unknownControlFlow.ts, 273, 21)) +} + +function ff2(t: T & {}, k: keyof T) { +>ff2 : Symbol(ff2, Decl(unknownControlFlow.ts, 275, 1)) +>T : Symbol(T, Decl(unknownControlFlow.ts, 277, 13)) +>t : Symbol(t, Decl(unknownControlFlow.ts, 277, 16)) +>T : Symbol(T, Decl(unknownControlFlow.ts, 277, 13)) +>k : Symbol(k, Decl(unknownControlFlow.ts, 277, 26)) +>T : Symbol(T, Decl(unknownControlFlow.ts, 277, 13)) + + t[k]; +>t : Symbol(t, Decl(unknownControlFlow.ts, 277, 16)) +>k : Symbol(k, Decl(unknownControlFlow.ts, 277, 26)) +} + +function ff3(t: T, k: keyof (T & {})) { +>ff3 : Symbol(ff3, Decl(unknownControlFlow.ts, 279, 1)) +>T : Symbol(T, Decl(unknownControlFlow.ts, 281, 13)) +>t : Symbol(t, Decl(unknownControlFlow.ts, 281, 16)) +>T : Symbol(T, Decl(unknownControlFlow.ts, 281, 13)) +>k : Symbol(k, Decl(unknownControlFlow.ts, 281, 21)) +>T : Symbol(T, Decl(unknownControlFlow.ts, 281, 13)) + + t[k]; // Error +>t : Symbol(t, Decl(unknownControlFlow.ts, 281, 16)) +>k : Symbol(k, Decl(unknownControlFlow.ts, 281, 21)) +} + +function ff4(t: T & {}, k: keyof (T & {})) { +>ff4 : Symbol(ff4, Decl(unknownControlFlow.ts, 283, 1)) +>T : Symbol(T, Decl(unknownControlFlow.ts, 285, 13)) +>t : Symbol(t, Decl(unknownControlFlow.ts, 285, 16)) +>T : Symbol(T, Decl(unknownControlFlow.ts, 285, 13)) +>k : Symbol(k, Decl(unknownControlFlow.ts, 285, 26)) +>T : Symbol(T, Decl(unknownControlFlow.ts, 285, 13)) + + t[k]; +>t : Symbol(t, Decl(unknownControlFlow.ts, 285, 16)) +>k : Symbol(k, Decl(unknownControlFlow.ts, 285, 26)) +} + +ff1(null, 'foo'); // Error +>ff1 : Symbol(ff1, Decl(unknownControlFlow.ts, 267, 1)) + +ff2(null, 'foo'); // Error +>ff2 : Symbol(ff2, Decl(unknownControlFlow.ts, 275, 1)) + +ff3(null, 'foo'); +>ff3 : Symbol(ff3, Decl(unknownControlFlow.ts, 279, 1)) + +ff4(null, 'foo'); // Error +>ff4 : Symbol(ff4, Decl(unknownControlFlow.ts, 283, 1)) + +// Repro from #49681 + +type Foo = { [key: string]: unknown }; +>Foo : Symbol(Foo, Decl(unknownControlFlow.ts, 292, 17)) +>key : Symbol(key, Decl(unknownControlFlow.ts, 296, 14)) + +type NullableFoo = Foo | undefined; +>NullableFoo : Symbol(NullableFoo, Decl(unknownControlFlow.ts, 296, 38)) +>Foo : Symbol(Foo, Decl(unknownControlFlow.ts, 292, 17)) + +type Bar = NonNullable[string]; +>Bar : Symbol(Bar, Decl(unknownControlFlow.ts, 297, 35)) +>T : Symbol(T, Decl(unknownControlFlow.ts, 299, 9)) +>NullableFoo : Symbol(NullableFoo, Decl(unknownControlFlow.ts, 296, 38)) +>NonNullable : Symbol(NonNullable, Decl(lib.es5.d.ts, --, --)) +>T : Symbol(T, Decl(unknownControlFlow.ts, 299, 9)) + diff --git a/tests/baselines/reference/unknownControlFlow.types b/tests/baselines/reference/unknownControlFlow.types index cfd88dbffc810..bd1d851c5b3a9 100644 --- a/tests/baselines/reference/unknownControlFlow.types +++ b/tests/baselines/reference/unknownControlFlow.types @@ -718,3 +718,87 @@ function foo(x: T | null) { } } +// We allow an unconstrained object of a generic type `T` to be indexed by a key of type `keyof T` +// without a check that the object is non-undefined and non-null. This is safe because `keyof T` +// is `never` (meaning no possible keys) for any `T` that includes `undefined` or `null`. + +function ff1(t: T, k: keyof T) { +>ff1 : (t: T, k: keyof T) => void +>t : T +>k : keyof T + + t[k]; +>t[k] : T[keyof T] +>t : T +>k : keyof T +} + +function ff2(t: T & {}, k: keyof T) { +>ff2 : (t: T & {}, k: keyof T) => void +>t : T & {} +>k : keyof T + + t[k]; +>t[k] : (T & {})[keyof T] +>t : T & {} +>k : keyof T +} + +function ff3(t: T, k: keyof (T & {})) { +>ff3 : (t: T, k: keyof (T & {})) => void +>t : T +>k : keyof (T & {}) + + t[k]; // Error +>t[k] : any +>t : T +>k : keyof (T & {}) +} + +function ff4(t: T & {}, k: keyof (T & {})) { +>ff4 : (t: T & {}, k: keyof (T & {})) => void +>t : T & {} +>k : keyof (T & {}) + + t[k]; +>t[k] : (T & {})[keyof (T & {})] +>t : T & {} +>k : keyof (T & {}) +} + +ff1(null, 'foo'); // Error +>ff1(null, 'foo') : void +>ff1 : (t: T, k: keyof T) => void +>null : null +>'foo' : "foo" + +ff2(null, 'foo'); // Error +>ff2(null, 'foo') : void +>ff2 : (t: T & {}, k: keyof T) => void +>null : null +>'foo' : "foo" + +ff3(null, 'foo'); +>ff3(null, 'foo') : void +>ff3 : (t: T, k: keyof (T & {})) => void +>null : null +>'foo' : "foo" + +ff4(null, 'foo'); // Error +>ff4(null, 'foo') : void +>ff4 : (t: T & {}, k: keyof (T & {})) => void +>null : null +>'foo' : "foo" + +// Repro from #49681 + +type Foo = { [key: string]: unknown }; +>Foo : { [key: string]: unknown; } +>key : string + +type NullableFoo = Foo | undefined; +>NullableFoo : Foo | undefined + +type Bar = NonNullable[string]; +>Bar : Bar + diff --git a/tests/cases/conformance/types/mapped/mappedTypes4.ts b/tests/cases/conformance/types/mapped/mappedTypes4.ts index 4def192d602d8..a2ff2ad4348b8 100644 --- a/tests/cases/conformance/types/mapped/mappedTypes4.ts +++ b/tests/cases/conformance/types/mapped/mappedTypes4.ts @@ -65,7 +65,7 @@ var x1: DeepReadonlyFoo; type Z = { a: number }; type Clone = { - [P in keyof (T & {})]: T[P]; + [P in keyof (T & {})]: (T & {})[P]; }; type M = Clone; // M should be { a: number } diff --git a/tests/cases/conformance/types/unknown/unknownControlFlow.ts b/tests/cases/conformance/types/unknown/unknownControlFlow.ts index 5bf8f0f2946da..55072dfddc470 100644 --- a/tests/cases/conformance/types/unknown/unknownControlFlow.ts +++ b/tests/cases/conformance/types/unknown/unknownControlFlow.ts @@ -269,3 +269,35 @@ function foo(x: T | null) { y; } } + +// We allow an unconstrained object of a generic type `T` to be indexed by a key of type `keyof T` +// without a check that the object is non-undefined and non-null. This is safe because `keyof T` +// is `never` (meaning no possible keys) for any `T` that includes `undefined` or `null`. + +function ff1(t: T, k: keyof T) { + t[k]; +} + +function ff2(t: T & {}, k: keyof T) { + t[k]; +} + +function ff3(t: T, k: keyof (T & {})) { + t[k]; // Error +} + +function ff4(t: T & {}, k: keyof (T & {})) { + t[k]; +} + +ff1(null, 'foo'); // Error +ff2(null, 'foo'); // Error +ff3(null, 'foo'); +ff4(null, 'foo'); // Error + +// Repro from #49681 + +type Foo = { [key: string]: unknown }; +type NullableFoo = Foo | undefined; + +type Bar = NonNullable[string];