Skip to content

Commit b7e8a6d

Browse files
authored
Merge pull request #12643 from Microsoft/keyofUnionIntersection
Properly handle unions and intersections with keyof T and T[K]
2 parents 23992ba + 3aa05b2 commit b7e8a6d

File tree

9 files changed

+1059
-187
lines changed

9 files changed

+1059
-187
lines changed

src/compiler/checker.ts

Lines changed: 10 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -4523,7 +4523,7 @@ namespace ts {
45234523
// First, if the constraint type is a type parameter, obtain the base constraint. Then,
45244524
// if the key type is a 'keyof X', obtain 'keyof C' where C is the base constraint of X.
45254525
// Finally, iterate over the constituents of the resulting iteration type.
4526-
const keyType = constraintType.flags & TypeFlags.TypeParameter ? getApparentType(constraintType) : constraintType;
4526+
const keyType = constraintType.flags & TypeFlags.TypeVariable ? getApparentType(constraintType) : constraintType;
45274527
const iterationType = keyType.flags & TypeFlags.Index ? getIndexType(getApparentType((<IndexType>keyType).type)) : keyType;
45284528
forEachType(iterationType, t => {
45294529
// Create a mapper from T to the current iteration type constituent. Then, if the
@@ -4579,7 +4579,7 @@ namespace ts {
45794579
function isGenericMappedType(type: Type) {
45804580
if (getObjectFlags(type) & ObjectFlags.Mapped) {
45814581
const constraintType = getConstraintTypeFromMappedType(<MappedType>type);
4582-
return !!(constraintType.flags & (TypeFlags.TypeParameter | TypeFlags.Index));
4582+
return maybeTypeOfKind(constraintType, TypeFlags.TypeVariable | TypeFlags.Index);
45834583
}
45844584
return false;
45854585
}
@@ -5912,7 +5912,7 @@ namespace ts {
59125912
return links.resolvedType;
59135913
}
59145914

5915-
function getIndexTypeForTypeVariable(type: TypeVariable) {
5915+
function getIndexTypeForGenericType(type: TypeVariable | UnionOrIntersectionType) {
59165916
if (!type.resolvedIndexType) {
59175917
type.resolvedIndexType = <IndexType>createType(TypeFlags.Index);
59185918
type.resolvedIndexType.type = type;
@@ -5931,7 +5931,7 @@ namespace ts {
59315931
}
59325932

59335933
function getIndexType(type: Type): Type {
5934-
return type.flags & TypeFlags.TypeVariable ? getIndexTypeForTypeVariable(<TypeVariable>type) :
5934+
return maybeTypeOfKind(type, TypeFlags.TypeVariable) ? getIndexTypeForGenericType(<TypeVariable | UnionOrIntersectionType>type) :
59355935
getObjectFlags(type) & ObjectFlags.Mapped ? getConstraintTypeFromMappedType(<MappedType>type) :
59365936
type.flags & TypeFlags.Any || getIndexInfoOfType(type, IndexKind.String) ? stringType :
59375937
getLiteralTypeFromPropertyNames(type);
@@ -6032,14 +6032,11 @@ namespace ts {
60326032
}
60336033

60346034
function getIndexedAccessType(objectType: Type, indexType: Type, accessNode?: ElementAccessExpression | IndexedAccessTypeNode) {
6035-
if (indexType.flags & TypeFlags.TypeVariable ||
6036-
objectType.flags & TypeFlags.TypeVariable && indexType.flags & TypeFlags.Index ||
6037-
isGenericMappedType(objectType)) {
6038-
// If the object type is a type variable (a type parameter or another indexed access type), if the
6039-
// index type is a type variable or an index type, or if the object type is a mapped type with a
6040-
// generic constraint, we are performing a higher-order index access where we cannot meaningfully
6041-
// access the properties of the object type. In those cases, we first check that the index type is
6042-
// assignable to 'keyof T' for the object type.
6035+
if (maybeTypeOfKind(indexType, TypeFlags.TypeVariable | TypeFlags.Index) || isGenericMappedType(objectType)) {
6036+
// If the index type is generic or if the object type is a mapped type with a generic constraint,
6037+
// we are performing a higher-order index access where we cannot meaningfully access the properties
6038+
// of the object type. In those cases, we first check that the index type is assignable to 'keyof T'
6039+
// for the object type.
60436040
if (accessNode) {
60446041
if (!isTypeAssignableTo(indexType, getIndexType(objectType))) {
60456042
error(accessNode, Diagnostics.Type_0_cannot_be_used_to_index_type_1, typeToString(indexType), typeToString(objectType));
@@ -6539,7 +6536,7 @@ namespace ts {
65396536
// union type A | undefined, we produce { [P in keyof A]: X } | undefined.
65406537
const constraintType = getConstraintTypeFromMappedType(type);
65416538
if (constraintType.flags & TypeFlags.Index) {
6542-
const typeVariable = <TypeParameter>(<IndexType>constraintType).type;
6539+
const typeVariable = (<IndexType>constraintType).type;
65436540
const mappedTypeVariable = instantiateType(typeVariable, mapper);
65446541
if (typeVariable !== mappedTypeVariable) {
65456542
return mapType(mappedTypeVariable, t => {

src/compiler/types.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2908,6 +2908,8 @@ namespace ts {
29082908
/* @internal */
29092909
resolvedProperties: SymbolTable; // Cache of resolved properties
29102910
/* @internal */
2911+
resolvedIndexType: IndexType;
2912+
/* @internal */
29112913
couldContainTypeVariables: boolean;
29122914
}
29132915

@@ -2991,7 +2993,7 @@ namespace ts {
29912993

29922994
// keyof T types (TypeFlags.Index)
29932995
export interface IndexType extends Type {
2994-
type: TypeVariable;
2996+
type: TypeVariable | UnionOrIntersectionType;
29952997
}
29962998

29972999
export const enum SignatureKind {

tests/baselines/reference/keyofAndIndexedAccess.js

Lines changed: 118 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -219,6 +219,36 @@ function f60<T>(source: T, target: T) {
219219
}
220220
}
221221

222+
function f70(func: <T, U>(k1: keyof (T | U), k2: keyof (T & U)) => void) {
223+
func<{ a: any, b: any }, { a: any, c: any }>('a', 'a');
224+
func<{ a: any, b: any }, { a: any, c: any }>('a', 'b');
225+
func<{ a: any, b: any }, { a: any, c: any }>('a', 'c');
226+
}
227+
228+
function f71(func: <T, U>(x: T, y: U) => Partial<T & U>) {
229+
let x = func({ a: 1, b: "hello" }, { c: true });
230+
x.a; // number | undefined
231+
x.b; // string | undefined
232+
x.c; // boolean | undefined
233+
}
234+
235+
function f72(func: <T, U, K extends keyof T | keyof U>(x: T, y: U, k: K) => (T & U)[K]) {
236+
let a = func({ a: 1, b: "hello" }, { c: true }, 'a'); // number
237+
let b = func({ a: 1, b: "hello" }, { c: true }, 'b'); // string
238+
let c = func({ a: 1, b: "hello" }, { c: true }, 'c'); // boolean
239+
}
240+
241+
function f73(func: <T, U, K extends keyof (T & U)>(x: T, y: U, k: K) => (T & U)[K]) {
242+
let a = func({ a: 1, b: "hello" }, { c: true }, 'a'); // number
243+
let b = func({ a: 1, b: "hello" }, { c: true }, 'b'); // string
244+
let c = func({ a: 1, b: "hello" }, { c: true }, 'c'); // boolean
245+
}
246+
247+
function f74(func: <T, U, K extends keyof (T | U)>(x: T, y: U, k: K) => (T | U)[K]) {
248+
let a = func({ a: 1, b: "hello" }, { a: 2, b: true }, 'a'); // number
249+
let b = func({ a: 1, b: "hello" }, { a: 2, b: true }, 'b'); // string | boolean
250+
}
251+
222252
// Repros from #12011
223253

224254
class Base {
@@ -291,7 +321,39 @@ var empty = one(() => {}) // inferred as {}, expected
291321
type Handlers<T> = { [K in keyof T]: (t: T[K]) => void }
292322
declare function on<T>(handlerHash: Handlers<T>): T
293323
var hashOfEmpty1 = on({ test: () => {} }); // {}
294-
var hashOfEmpty2 = on({ test: (x: boolean) => {} }); // { test: boolean }
324+
var hashOfEmpty2 = on({ test: (x: boolean) => {} }); // { test: boolean }
325+
326+
// Repro from #12624
327+
328+
interface Options1<Data, Computed> {
329+
data?: Data
330+
computed?: Computed;
331+
}
332+
333+
declare class Component1<Data, Computed> {
334+
constructor(options: Options1<Data, Computed>);
335+
get<K extends keyof (Data & Computed)>(key: K): (Data & Computed)[K];
336+
}
337+
338+
let c1 = new Component1({
339+
data: {
340+
hello: ""
341+
}
342+
});
343+
344+
c1.get("hello");
345+
346+
// Repro from #12625
347+
348+
interface Options2<Data, Computed> {
349+
data?: Data
350+
computed?: Computed;
351+
}
352+
353+
declare class Component2<Data, Computed> {
354+
constructor(options: Options2<Data, Computed>);
355+
get<K extends keyof Data | keyof Computed>(key: K): (Data & Computed)[K];
356+
}
295357

296358
//// [keyofAndIndexedAccess.js]
297359
var __extends = (this && this.__extends) || function (d, b) {
@@ -438,6 +500,31 @@ function f60(source, target) {
438500
target[k] = source[k];
439501
}
440502
}
503+
function f70(func) {
504+
func('a', 'a');
505+
func('a', 'b');
506+
func('a', 'c');
507+
}
508+
function f71(func) {
509+
var x = func({ a: 1, b: "hello" }, { c: true });
510+
x.a; // number | undefined
511+
x.b; // string | undefined
512+
x.c; // boolean | undefined
513+
}
514+
function f72(func) {
515+
var a = func({ a: 1, b: "hello" }, { c: true }, 'a'); // number
516+
var b = func({ a: 1, b: "hello" }, { c: true }, 'b'); // string
517+
var c = func({ a: 1, b: "hello" }, { c: true }, 'c'); // boolean
518+
}
519+
function f73(func) {
520+
var a = func({ a: 1, b: "hello" }, { c: true }, 'a'); // number
521+
var b = func({ a: 1, b: "hello" }, { c: true }, 'b'); // string
522+
var c = func({ a: 1, b: "hello" }, { c: true }, 'c'); // boolean
523+
}
524+
function f74(func) {
525+
var a = func({ a: 1, b: "hello" }, { a: 2, b: true }, 'a'); // number
526+
var b = func({ a: 1, b: "hello" }, { a: 2, b: true }, 'b'); // string | boolean
527+
}
441528
// Repros from #12011
442529
var Base = (function () {
443530
function Base() {
@@ -496,6 +583,12 @@ var assignTo2 = function (object, key1, key2) {
496583
var empty = one(function () { }); // inferred as {}, expected
497584
var hashOfEmpty1 = on({ test: function () { } }); // {}
498585
var hashOfEmpty2 = on({ test: function (x) { } }); // { test: boolean }
586+
var c1 = new Component1({
587+
data: {
588+
hello: ""
589+
}
590+
});
591+
c1.get("hello");
499592

500593

501594
//// [keyofAndIndexedAccess.d.ts]
@@ -603,6 +696,11 @@ declare function f53<T, K extends keyof T>(obj: {
603696
declare function f54<T>(obj: T, key: keyof T): void;
604697
declare function f55<T, K extends keyof T>(obj: T, key: K): void;
605698
declare function f60<T>(source: T, target: T): void;
699+
declare function f70(func: <T, U>(k1: keyof (T | U), k2: keyof (T & U)) => void): void;
700+
declare function f71(func: <T, U>(x: T, y: U) => Partial<T & U>): void;
701+
declare function f72(func: <T, U, K extends keyof T | keyof U>(x: T, y: U, k: K) => (T & U)[K]): void;
702+
declare function f73(func: <T, U, K extends keyof (T & U)>(x: T, y: U, k: K) => (T & U)[K]): void;
703+
declare function f74(func: <T, U, K extends keyof (T | U)>(x: T, y: U, k: K) => (T | U)[K]): void;
606704
declare class Base {
607705
get<K extends keyof this>(prop: K): this[K];
608706
set<K extends keyof this>(prop: K, value: this[K]): void;
@@ -640,3 +738,22 @@ declare var hashOfEmpty1: {};
640738
declare var hashOfEmpty2: {
641739
test: boolean;
642740
};
741+
interface Options1<Data, Computed> {
742+
data?: Data;
743+
computed?: Computed;
744+
}
745+
declare class Component1<Data, Computed> {
746+
constructor(options: Options1<Data, Computed>);
747+
get<K extends keyof (Data & Computed)>(key: K): (Data & Computed)[K];
748+
}
749+
declare let c1: Component1<{
750+
hello: string;
751+
}, {}>;
752+
interface Options2<Data, Computed> {
753+
data?: Data;
754+
computed?: Computed;
755+
}
756+
declare class Component2<Data, Computed> {
757+
constructor(options: Options2<Data, Computed>);
758+
get<K extends keyof Data | keyof Computed>(key: K): (Data & Computed)[K];
759+
}

0 commit comments

Comments
 (0)