Skip to content

Commit df6c603

Browse files
committed
Infer type parameters from indexes on those parameters
1 parent 82502ea commit df6c603

File tree

9 files changed

+221
-23
lines changed

9 files changed

+221
-23
lines changed

src/compiler/checker.ts

Lines changed: 68 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -2617,8 +2617,8 @@ namespace ts {
26172617

26182618
function createMappedTypeNodeFromType(type: MappedType) {
26192619
Debug.assert(!!(type.flags & TypeFlags.Object));
2620-
const readonlyToken = type.declaration && type.declaration.readonlyToken ? createToken(SyntaxKind.ReadonlyKeyword) : undefined;
2621-
const questionToken = type.declaration && type.declaration.questionToken ? createToken(SyntaxKind.QuestionToken) : undefined;
2620+
const readonlyToken = isReadonlyMappedType(type) ? createToken(SyntaxKind.ReadonlyKeyword) : undefined;
2621+
const questionToken = isOptionalMappedType(type) ? createToken(SyntaxKind.QuestionToken) : undefined;
26222622
const typeParameterNode = typeParameterToDeclaration(getTypeParameterFromMappedType(type), context);
26232623
const templateTypeNode = typeToTypeNodeHelper(getTemplateTypeFromMappedType(type), context);
26242624

@@ -3689,7 +3689,7 @@ namespace ts {
36893689
writePunctuation(writer, SyntaxKind.OpenBraceToken);
36903690
writer.writeLine();
36913691
writer.increaseIndent();
3692-
if (type.declaration.readonlyToken) {
3692+
if (isReadonlyMappedType(type)) {
36933693
writeKeyword(writer, SyntaxKind.ReadonlyKeyword);
36943694
writeSpace(writer);
36953695
}
@@ -3700,7 +3700,7 @@ namespace ts {
37003700
writeSpace(writer);
37013701
writeType(getConstraintTypeFromMappedType(type), TypeFormatFlags.None);
37023702
writePunctuation(writer, SyntaxKind.CloseBracketToken);
3703-
if (type.declaration.questionToken) {
3703+
if (isOptionalMappedType(type)) {
37043704
writePunctuation(writer, SyntaxKind.QuestionToken);
37053705
}
37063706
writePunctuation(writer, SyntaxKind.ColonToken);
@@ -6032,11 +6032,9 @@ namespace ts {
60326032
const constraintType = getConstraintTypeFromMappedType(type);
60336033
const templateType = getTemplateTypeFromMappedType(type);
60346034
const modifiersType = getApparentType(getModifiersTypeFromMappedType(type)); // The 'T' in 'keyof T'
6035-
const templateReadonly = !!type.declaration.readonlyToken;
6036-
const templateOptional = !!type.declaration.questionToken;
6037-
const constraintDeclaration = type.declaration.typeParameter.constraint;
6038-
if (constraintDeclaration.kind === SyntaxKind.TypeOperator &&
6039-
(<TypeOperatorNode>constraintDeclaration).operator === SyntaxKind.KeyOfKeyword) {
6035+
const templateReadonly = isReadonlyMappedType(type);
6036+
const templateOptional = isOptionalMappedType(type);
6037+
if (isPossiblyHomomorphicMappedType(type)) {
60406038
// We have a { [P in keyof T]: X }
60416039
for (const propertySymbol of getPropertiesOfType(modifiersType)) {
60426040
addMemberForKeyType(getLiteralTypeFromPropertyName(propertySymbol), propertySymbol);
@@ -6104,15 +6102,14 @@ namespace ts {
61046102
function getTemplateTypeFromMappedType(type: MappedType) {
61056103
return type.templateType ||
61066104
(type.templateType = type.declaration.type ?
6107-
instantiateType(addOptionality(getTypeFromTypeNode(type.declaration.type), !!type.declaration.questionToken), type.mapper || identityMapper) :
6105+
instantiateType(addOptionality(getTypeFromTypeNode(type.declaration.type), isOptionalMappedType(type)), type.mapper || identityMapper) :
61086106
unknownType);
61096107
}
61106108

61116109
function getModifiersTypeFromMappedType(type: MappedType) {
61126110
if (!type.modifiersType) {
6113-
const constraintDeclaration = type.declaration.typeParameter.constraint;
6114-
if (constraintDeclaration.kind === SyntaxKind.TypeOperator &&
6115-
(<TypeOperatorNode>constraintDeclaration).operator === SyntaxKind.KeyOfKeyword) {
6111+
if (isPossiblyHomomorphicMappedType(type)) {
6112+
const constraintDeclaration = type.declaration.typeParameter.constraint;
61166113
// If the constraint declaration is a 'keyof T' node, the modifiers type is T. We check
61176114
// AST nodes here because, when T is a non-generic type, the logic below eagerly resolves
61186115
// 'keyof T' to a literal union type and we can't recover T from that type.
@@ -6132,8 +6129,8 @@ namespace ts {
61326129
}
61336130

61346131
function getMappedTypeModifiers(type: MappedType): MappedTypeModifiers {
6135-
return (type.declaration.readonlyToken ? MappedTypeModifiers.Readonly : 0) |
6136-
(type.declaration.questionToken ? MappedTypeModifiers.Optional : 0);
6132+
return (isReadonlyMappedType(type) ? MappedTypeModifiers.Readonly : 0) |
6133+
(isOptionalMappedType(type) ? MappedTypeModifiers.Optional : 0);
61376134
}
61386135

61396136
function getCombinedMappedTypeModifiers(type: MappedType): MappedTypeModifiers {
@@ -6143,7 +6140,7 @@ namespace ts {
61436140
}
61446141

61456142
function isPartialMappedType(type: Type) {
6146-
return getObjectFlags(type) & ObjectFlags.Mapped && !!(<MappedType>type).declaration.questionToken;
6143+
return getObjectFlags(type) & ObjectFlags.Mapped && isOptionalMappedType(type as MappedType);
61476144
}
61486145

61496146
function isGenericMappedType(type: Type) {
@@ -9738,7 +9735,7 @@ namespace ts {
97389735
if (target.flags & TypeFlags.TypeParameter) {
97399736
// A source type { [P in keyof T]: X } is related to a target type T if X is related to T[P].
97409737
if (getObjectFlags(source) & ObjectFlags.Mapped && getConstraintTypeFromMappedType(<MappedType>source) === getIndexType(target)) {
9741-
if (!(<MappedType>source).declaration.questionToken) {
9738+
if (!isOptionalMappedType(<MappedType>source)) {
97429739
const templateType = getTemplateTypeFromMappedType(<MappedType>source);
97439740
const indexedAccessType = getIndexedAccessType(target, getTypeParameterFromMappedType(<MappedType>source));
97449741
if (result = isRelatedTo(templateType, indexedAccessType, reportErrors)) {
@@ -11050,7 +11047,8 @@ namespace ts {
1105011047
inferredType: undefined,
1105111048
priority: undefined,
1105211049
topLevel: true,
11053-
isFixed: false
11050+
isFixed: false,
11051+
indexes: undefined,
1105411052
};
1105511053
}
1105611054

@@ -11061,7 +11059,8 @@ namespace ts {
1106111059
inferredType: inference.inferredType,
1106211060
priority: inference.priority,
1106311061
topLevel: inference.topLevel,
11064-
isFixed: inference.isFixed
11062+
isFixed: inference.isFixed,
11063+
indexes: inference.indexes && inference.indexes.slice(),
1106511064
};
1106611065
}
1106711066

@@ -11124,8 +11123,8 @@ namespace ts {
1112411123
const inference = createInferenceInfo(typeParameter);
1112511124
const inferences = [inference];
1112611125
const templateType = getTemplateTypeFromMappedType(target);
11127-
const readonlyMask = target.declaration.readonlyToken ? false : true;
11128-
const optionalMask = target.declaration.questionToken ? 0 : SymbolFlags.Optional;
11126+
const readonlyMask = isReadonlyMappedType(target) ? false : true;
11127+
const optionalMask = isOptionalMappedType(target) ? 0 : SymbolFlags.Optional;
1112911128
const members = createSymbolTable();
1113011129
for (const prop of properties) {
1113111130
const propType = getTypeOfSymbol(prop);
@@ -11252,6 +11251,27 @@ namespace ts {
1125211251
}
1125311252
return;
1125411253
}
11254+
else if (target.flags & TypeFlags.IndexedAccess) {
11255+
const targetConstraint = (<IndexedAccessType>target).objectType;
11256+
const inference = getInferenceInfoForType(targetConstraint);
11257+
if (inference) {
11258+
if (!inference.isFixed) {
11259+
const map = <MappedType>createObjectType(ObjectFlags.Mapped);
11260+
map.templateType = source;
11261+
map.constraintType = (<IndexedAccessType>target).indexType;
11262+
map.typeParameter = <TypeParameter>createType(TypeFlags.TypeParameter);
11263+
// TODO (weswigham): Ensure the name chosen for the unused "K" does not shadow any other type variables in the given scope, so as to not have a chance of breaking declaration emit
11264+
map.typeParameter.symbol = createSymbol(SymbolFlags.TypeParameter, "K" as __String);
11265+
map.typeParameter.constraint = map.constraintType;
11266+
map.modifiersType = (<IndexedAccessType>target).indexType;
11267+
map.hasQuestionToken = false;
11268+
map.hasReadonlyToken = false;
11269+
map.hasPossiblyHomomorphicConstraint = false;
11270+
(inference.indexes || (inference.indexes = [])).push(map);
11271+
}
11272+
return;
11273+
}
11274+
}
1125511275
}
1125611276
else if (getObjectFlags(source) & ObjectFlags.Reference && getObjectFlags(target) & ObjectFlags.Reference && (<TypeReference>source).target === (<TypeReference>target).target) {
1125711277
// If source and target are references to the same generic type, infer from type arguments
@@ -11510,6 +11530,10 @@ namespace ts {
1151011530
const inference = context.inferences[index];
1151111531
let inferredType = inference.inferredType;
1151211532
if (!inferredType) {
11533+
if (inference.indexes) {
11534+
// Build a candidate from all indexes
11535+
(inference.candidates || (inference.candidates = [])).push(getIntersectionType(inference.indexes));
11536+
}
1151311537
if (inference.candidates) {
1151411538
// Extract all object literal types and replace them with a single widened and normalized type.
1151511539
const candidates = widenObjectLiteralCandidates(inference.candidates);
@@ -19818,6 +19842,28 @@ namespace ts {
1981819842
forEach(node.types, checkSourceElement);
1981919843
}
1982019844

19845+
function isReadonlyMappedType(type: MappedType) {
19846+
if (type.hasReadonlyToken === undefined) {
19847+
type.hasReadonlyToken = !!type.declaration.readonlyToken;
19848+
}
19849+
return type.hasReadonlyToken;
19850+
}
19851+
19852+
function isOptionalMappedType(type: MappedType) {
19853+
if (type.hasQuestionToken === undefined) {
19854+
type.hasQuestionToken = !!type.declaration.questionToken;
19855+
}
19856+
return type.hasQuestionToken;
19857+
}
19858+
19859+
function isPossiblyHomomorphicMappedType(type: MappedType) {
19860+
if (type.hasPossiblyHomomorphicConstraint === undefined) {
19861+
const constraint = type.declaration.typeParameter.constraint;
19862+
type.hasPossiblyHomomorphicConstraint = isTypeOperatorNode(constraint) && constraint.operator === SyntaxKind.KeyOfKeyword;
19863+
}
19864+
return type.hasPossiblyHomomorphicConstraint;
19865+
}
19866+
1982119867
function checkIndexedAccessIndexType(type: Type, accessNode: ElementAccessExpression | IndexedAccessTypeNode) {
1982219868
if (!(type.flags & TypeFlags.IndexedAccess)) {
1982319869
return type;
@@ -19827,7 +19873,7 @@ namespace ts {
1982719873
const indexType = (<IndexedAccessType>type).indexType;
1982819874
if (isTypeAssignableTo(indexType, getIndexType(objectType))) {
1982919875
if (accessNode.kind === SyntaxKind.ElementAccessExpression && isAssignmentTarget(accessNode) &&
19830-
getObjectFlags(objectType) & ObjectFlags.Mapped && (<MappedType>objectType).declaration.readonlyToken) {
19876+
getObjectFlags(objectType) & ObjectFlags.Mapped && isReadonlyMappedType(<MappedType>objectType)) {
1983119877
error(accessNode, Diagnostics.Index_signature_in_type_0_only_permits_reading, typeToString(objectType));
1983219878
}
1983319879
return type;

src/compiler/types.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3521,6 +3521,9 @@ namespace ts {
35213521
constraintType?: Type;
35223522
templateType?: Type;
35233523
modifiersType?: Type;
3524+
hasQuestionToken?: boolean;
3525+
hasReadonlyToken?: boolean;
3526+
hasPossiblyHomomorphicConstraint?: boolean;
35243527
}
35253528

35263529
export interface EvolvingArrayType extends ObjectType {
@@ -3665,6 +3668,7 @@ namespace ts {
36653668
export interface InferenceInfo {
36663669
typeParameter: TypeParameter;
36673670
candidates: Type[];
3671+
indexes: Type[]; // Partial candidates created by indexed accesses
36683672
inferredType: Type;
36693673
priority: InferencePriority;
36703674
topLevel: boolean;

tests/baselines/reference/api/tsserverlibrary.d.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2155,6 +2155,7 @@ declare namespace ts {
21552155
interface InferenceInfo {
21562156
typeParameter: TypeParameter;
21572157
candidates: Type[];
2158+
indexes: Type[];
21582159
inferredType: Type;
21592160
priority: InferencePriority;
21602161
topLevel: boolean;

tests/baselines/reference/api/typescript.d.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2155,6 +2155,7 @@ declare namespace ts {
21552155
interface InferenceInfo {
21562156
typeParameter: TypeParameter;
21572157
candidates: Type[];
2158+
indexes: Type[];
21582159
inferredType: Type;
21592160
priority: InferencePriority;
21602161
topLevel: boolean;
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
//// [indexAccessCombinedInference.ts]
2+
interface Args {
3+
TA: object,
4+
TY: object
5+
}
6+
7+
function foo<T extends Args>(
8+
a: T["TA"],
9+
b: T["TY"]): T["TA"] & T["TY"] {
10+
return undefined!;
11+
}
12+
13+
const x = foo({
14+
x: {
15+
j: 12,
16+
i: 11
17+
}
18+
}, { y: 42 });
19+
20+
//// [indexAccessCombinedInference.js]
21+
function foo(a, b) {
22+
return undefined;
23+
}
24+
var x = foo({
25+
x: {
26+
j: 12,
27+
i: 11
28+
}
29+
}, { y: 42 });
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
=== tests/cases/compiler/indexAccessCombinedInference.ts ===
2+
interface Args {
3+
>Args : Symbol(Args, Decl(indexAccessCombinedInference.ts, 0, 0))
4+
5+
TA: object,
6+
>TA : Symbol(Args.TA, Decl(indexAccessCombinedInference.ts, 0, 16))
7+
8+
TY: object
9+
>TY : Symbol(Args.TY, Decl(indexAccessCombinedInference.ts, 1, 15))
10+
}
11+
12+
function foo<T extends Args>(
13+
>foo : Symbol(foo, Decl(indexAccessCombinedInference.ts, 3, 1))
14+
>T : Symbol(T, Decl(indexAccessCombinedInference.ts, 5, 13))
15+
>Args : Symbol(Args, Decl(indexAccessCombinedInference.ts, 0, 0))
16+
17+
a: T["TA"],
18+
>a : Symbol(a, Decl(indexAccessCombinedInference.ts, 5, 29))
19+
>T : Symbol(T, Decl(indexAccessCombinedInference.ts, 5, 13))
20+
21+
b: T["TY"]): T["TA"] & T["TY"] {
22+
>b : Symbol(b, Decl(indexAccessCombinedInference.ts, 6, 15))
23+
>T : Symbol(T, Decl(indexAccessCombinedInference.ts, 5, 13))
24+
>T : Symbol(T, Decl(indexAccessCombinedInference.ts, 5, 13))
25+
>T : Symbol(T, Decl(indexAccessCombinedInference.ts, 5, 13))
26+
27+
return undefined!;
28+
>undefined : Symbol(undefined)
29+
}
30+
31+
const x = foo({
32+
>x : Symbol(x, Decl(indexAccessCombinedInference.ts, 11, 5))
33+
>foo : Symbol(foo, Decl(indexAccessCombinedInference.ts, 3, 1))
34+
35+
x: {
36+
>x : Symbol(x, Decl(indexAccessCombinedInference.ts, 11, 15))
37+
38+
j: 12,
39+
>j : Symbol(j, Decl(indexAccessCombinedInference.ts, 12, 8))
40+
41+
i: 11
42+
>i : Symbol(i, Decl(indexAccessCombinedInference.ts, 13, 14))
43+
}
44+
}, { y: 42 });
45+
>y : Symbol(y, Decl(indexAccessCombinedInference.ts, 16, 4))
46+
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
=== tests/cases/compiler/indexAccessCombinedInference.ts ===
2+
interface Args {
3+
>Args : Args
4+
5+
TA: object,
6+
>TA : object
7+
8+
TY: object
9+
>TY : object
10+
}
11+
12+
function foo<T extends Args>(
13+
>foo : <T extends Args>(a: T["TA"], b: T["TY"]) => T["TA"] & T["TY"]
14+
>T : T
15+
>Args : Args
16+
17+
a: T["TA"],
18+
>a : T["TA"]
19+
>T : T
20+
21+
b: T["TY"]): T["TA"] & T["TY"] {
22+
>b : T["TY"]
23+
>T : T
24+
>T : T
25+
>T : T
26+
27+
return undefined!;
28+
>undefined! : undefined
29+
>undefined : undefined
30+
}
31+
32+
const x = foo({
33+
>x : { x: { j: number; i: number; }; } & { y: number; }
34+
>foo({ x: { j: 12, i: 11 }}, { y: 42 }) : { x: { j: number; i: number; }; } & { y: number; }
35+
>foo : <T extends Args>(a: T["TA"], b: T["TY"]) => T["TA"] & T["TY"]
36+
>{ x: { j: 12, i: 11 }} : { x: { j: number; i: number; }; }
37+
38+
x: {
39+
>x : { j: number; i: number; }
40+
>{ j: 12, i: 11 } : { j: number; i: number; }
41+
42+
j: 12,
43+
>j : number
44+
>12 : 12
45+
46+
i: 11
47+
>i : number
48+
>11 : 11
49+
}
50+
}, { y: 42 });
51+
>{ y: 42 } : { y: number; }
52+
>y : number
53+
>42 : 42
54+

tests/baselines/reference/inferingFromAny.types

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -293,7 +293,7 @@ var a = f18(a);
293293

294294
var a = f19(a, a);
295295
>a : any
296-
>f19(a, a) : any
296+
>f19(a, a) : { [K in K]: any; }
297297
>f19 : <T, K extends keyof T>(k: K, x: T[K]) => T
298298
>a : any
299299
>a : any
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
interface Args {
2+
TA: object,
3+
TY: object
4+
}
5+
6+
function foo<T extends Args>(
7+
a: T["TA"],
8+
b: T["TY"]): T["TA"] & T["TY"] {
9+
return undefined!;
10+
}
11+
12+
const x = foo({
13+
x: {
14+
j: 12,
15+
i: 11
16+
}
17+
}, { y: 42 });

0 commit comments

Comments
 (0)