Skip to content

Commit eaabf92

Browse files
authored
Combine keyof T inferences (microsoft#22525)
* Combine keyof T inferences * Extract covariant inference derivation into function * Test:keyof inference lower priority than return inference for microsoft#22376 * Update 'expected' comment in keyofInferenceLowerPriorityThanReturn * Update comment in test too, not just baselines * Fix typo * Move tests
1 parent b56093f commit eaabf92

12 files changed

+552
-28
lines changed

src/compiler/checker.ts

+30-19
Original file line numberDiff line numberDiff line change
@@ -11704,7 +11704,10 @@ namespace ts {
1170411704
else if ((isLiteralType(source) || source.flags & TypeFlags.String) && target.flags & TypeFlags.Index) {
1170511705
const empty = createEmptyObjectTypeFromStringLiteral(source);
1170611706
contravariant = !contravariant;
11707+
const savePriority = priority;
11708+
priority |= InferencePriority.LiteralKeyof;
1170711709
inferFromTypes(empty, (target as IndexType).type);
11710+
priority = savePriority;
1170811711
contravariant = !contravariant;
1170911712
}
1171011713
else if (source.flags & TypeFlags.IndexedAccess && target.flags & TypeFlags.IndexedAccess) {
@@ -11947,39 +11950,47 @@ namespace ts {
1194711950
return candidates;
1194811951
}
1194911952

11953+
function getContravariantInference(inference: InferenceInfo) {
11954+
return inference.priority & InferencePriority.PriorityImpliesCombination ? getIntersectionType(inference.contraCandidates) : getCommonSubtype(inference.contraCandidates);
11955+
}
11956+
11957+
function getCovariantInference(inference: InferenceInfo, context: InferenceContext, signature: Signature) {
11958+
// Extract all object literal types and replace them with a single widened and normalized type.
11959+
const candidates = widenObjectLiteralCandidates(inference.candidates);
11960+
// We widen inferred literal types if
11961+
// all inferences were made to top-level occurrences of the type parameter, and
11962+
// the type parameter has no constraint or its constraint includes no primitive or literal types, and
11963+
// the type parameter was fixed during inference or does not occur at top-level in the return type.
11964+
const widenLiteralTypes = inference.topLevel &&
11965+
!hasPrimitiveConstraint(inference.typeParameter) &&
11966+
(inference.isFixed || !isTypeParameterAtTopLevel(getReturnTypeOfSignature(signature), inference.typeParameter));
11967+
const baseCandidates = widenLiteralTypes ? sameMap(candidates, getWidenedLiteralType) : candidates;
11968+
// If all inferences were made from contravariant positions, infer a common subtype. Otherwise, if
11969+
// union types were requested or if all inferences were made from the return type position, infer a
11970+
// union type. Otherwise, infer a common supertype.
11971+
const unwidenedType = context.flags & InferenceFlags.InferUnionTypes || inference.priority & InferencePriority.PriorityImpliesCombination ?
11972+
getUnionType(baseCandidates, UnionReduction.Subtype) :
11973+
getCommonSupertype(baseCandidates);
11974+
return getWidenedType(unwidenedType);
11975+
}
11976+
1195011977
function getInferredType(context: InferenceContext, index: number): Type {
1195111978
const inference = context.inferences[index];
1195211979
let inferredType = inference.inferredType;
1195311980
if (!inferredType) {
1195411981
const signature = context.signature;
1195511982
if (signature) {
1195611983
if (inference.candidates) {
11957-
// Extract all object literal types and replace them with a single widened and normalized type.
11958-
const candidates = widenObjectLiteralCandidates(inference.candidates);
11959-
// We widen inferred literal types if
11960-
// all inferences were made to top-level ocurrences of the type parameter, and
11961-
// the type parameter has no constraint or its constraint includes no primitive or literal types, and
11962-
// the type parameter was fixed during inference or does not occur at top-level in the return type.
11963-
const widenLiteralTypes = inference.topLevel &&
11964-
!hasPrimitiveConstraint(inference.typeParameter) &&
11965-
(inference.isFixed || !isTypeParameterAtTopLevel(getReturnTypeOfSignature(signature), inference.typeParameter));
11966-
const baseCandidates = widenLiteralTypes ? sameMap(candidates, getWidenedLiteralType) : candidates;
11967-
// If all inferences were made from contravariant positions, infer a common subtype. Otherwise, if
11968-
// union types were requested or if all inferences were made from the return type position, infer a
11969-
// union type. Otherwise, infer a common supertype.
11970-
const unwidenedType = context.flags & InferenceFlags.InferUnionTypes || inference.priority & InferencePriority.PriorityImpliesUnion ?
11971-
getUnionType(baseCandidates, UnionReduction.Subtype) :
11972-
getCommonSupertype(baseCandidates);
11973-
inferredType = getWidenedType(unwidenedType);
11984+
inferredType = getCovariantInference(inference, context, signature);
1197411985
// If we have inferred 'never' but have contravariant candidates. To get a more specific type we
1197511986
// infer from the contravariant candidates instead.
1197611987
if (inferredType.flags & TypeFlags.Never && inference.contraCandidates) {
11977-
inferredType = getCommonSubtype(inference.contraCandidates);
11988+
inferredType = getContravariantInference(inference);
1197811989
}
1197911990
}
1198011991
else if (inference.contraCandidates) {
1198111992
// We only have contravariant inferences, infer the best common subtype of those
11982-
inferredType = getCommonSubtype(inference.contraCandidates);
11993+
inferredType = getContravariantInference(inference);
1198311994
}
1198411995
else if (context.flags & InferenceFlags.NoDefault) {
1198511996
// We use silentNeverType as the wildcard that signals no inferences.

src/compiler/types.ts

+4-3
Original file line numberDiff line numberDiff line change
@@ -3945,10 +3945,11 @@ namespace ts {
39453945
HomomorphicMappedType = 1 << 1, // Reverse inference for homomorphic mapped type
39463946
MappedTypeConstraint = 1 << 2, // Reverse inference for mapped type
39473947
ReturnType = 1 << 3, // Inference made from return type of generic function
3948-
NoConstraints = 1 << 4, // Don't infer from constraints of instantiable types
3949-
AlwaysStrict = 1 << 5, // Always use strict rules for contravariant inferences
3948+
LiteralKeyof = 1 << 4, // Inference made from a string literal to a keyof T
3949+
NoConstraints = 1 << 5, // Don't infer from constraints of instantiable types
3950+
AlwaysStrict = 1 << 6, // Always use strict rules for contravariant inferences
39503951

3951-
PriorityImpliesUnion = ReturnType | MappedTypeConstraint, // These priorities imply that the resulting type should be a union of all candidates
3952+
PriorityImpliesCombination = ReturnType | MappedTypeConstraint | LiteralKeyof, // These priorities imply that the resulting type should be a combination of all candidates
39523953
}
39533954

39543955
/* @internal */

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

+4-3
Original file line numberDiff line numberDiff line change
@@ -2242,9 +2242,10 @@ declare namespace ts {
22422242
HomomorphicMappedType = 2,
22432243
MappedTypeConstraint = 4,
22442244
ReturnType = 8,
2245-
NoConstraints = 16,
2246-
AlwaysStrict = 32,
2247-
PriorityImpliesUnion = 12
2245+
LiteralKeyof = 16,
2246+
NoConstraints = 32,
2247+
AlwaysStrict = 64,
2248+
PriorityImpliesCombination = 28
22482249
}
22492250
interface JsFileExtensionInfo {
22502251
extension: string;

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

+4-3
Original file line numberDiff line numberDiff line change
@@ -2242,9 +2242,10 @@ declare namespace ts {
22422242
HomomorphicMappedType = 2,
22432243
MappedTypeConstraint = 4,
22442244
ReturnType = 8,
2245-
NoConstraints = 16,
2246-
AlwaysStrict = 32,
2247-
PriorityImpliesUnion = 12
2245+
LiteralKeyof = 16,
2246+
NoConstraints = 32,
2247+
AlwaysStrict = 64,
2248+
PriorityImpliesCombination = 28
22482249
}
22492250
interface JsFileExtensionInfo {
22502251
extension: string;
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
//// [keyofInferenceIntersectsResults.ts]
2+
interface X {
3+
a: string;
4+
b: string;
5+
}
6+
7+
declare function foo<T = X>(x: keyof T, y: keyof T): T;
8+
declare function bar<T>(x: keyof T, y: keyof T): T;
9+
10+
const a = foo<X>('a', 'b'); // compiles cleanly
11+
const b = foo('a', 'b'); // also clean
12+
const c = bar('a', 'b'); // still clean
13+
14+
//// [keyofInferenceIntersectsResults.js]
15+
var a = foo('a', 'b'); // compiles cleanly
16+
var b = foo('a', 'b'); // also clean
17+
var c = bar('a', 'b'); // still clean
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
=== tests/cases/conformance/types/typeRelationships/typeInference/keyofInferenceIntersectsResults.ts ===
2+
interface X {
3+
>X : Symbol(X, Decl(keyofInferenceIntersectsResults.ts, 0, 0))
4+
5+
a: string;
6+
>a : Symbol(X.a, Decl(keyofInferenceIntersectsResults.ts, 0, 13))
7+
8+
b: string;
9+
>b : Symbol(X.b, Decl(keyofInferenceIntersectsResults.ts, 1, 14))
10+
}
11+
12+
declare function foo<T = X>(x: keyof T, y: keyof T): T;
13+
>foo : Symbol(foo, Decl(keyofInferenceIntersectsResults.ts, 3, 1))
14+
>T : Symbol(T, Decl(keyofInferenceIntersectsResults.ts, 5, 21))
15+
>X : Symbol(X, Decl(keyofInferenceIntersectsResults.ts, 0, 0))
16+
>x : Symbol(x, Decl(keyofInferenceIntersectsResults.ts, 5, 28))
17+
>T : Symbol(T, Decl(keyofInferenceIntersectsResults.ts, 5, 21))
18+
>y : Symbol(y, Decl(keyofInferenceIntersectsResults.ts, 5, 39))
19+
>T : Symbol(T, Decl(keyofInferenceIntersectsResults.ts, 5, 21))
20+
>T : Symbol(T, Decl(keyofInferenceIntersectsResults.ts, 5, 21))
21+
22+
declare function bar<T>(x: keyof T, y: keyof T): T;
23+
>bar : Symbol(bar, Decl(keyofInferenceIntersectsResults.ts, 5, 55))
24+
>T : Symbol(T, Decl(keyofInferenceIntersectsResults.ts, 6, 21))
25+
>x : Symbol(x, Decl(keyofInferenceIntersectsResults.ts, 6, 24))
26+
>T : Symbol(T, Decl(keyofInferenceIntersectsResults.ts, 6, 21))
27+
>y : Symbol(y, Decl(keyofInferenceIntersectsResults.ts, 6, 35))
28+
>T : Symbol(T, Decl(keyofInferenceIntersectsResults.ts, 6, 21))
29+
>T : Symbol(T, Decl(keyofInferenceIntersectsResults.ts, 6, 21))
30+
31+
const a = foo<X>('a', 'b'); // compiles cleanly
32+
>a : Symbol(a, Decl(keyofInferenceIntersectsResults.ts, 8, 5))
33+
>foo : Symbol(foo, Decl(keyofInferenceIntersectsResults.ts, 3, 1))
34+
>X : Symbol(X, Decl(keyofInferenceIntersectsResults.ts, 0, 0))
35+
36+
const b = foo('a', 'b'); // also clean
37+
>b : Symbol(b, Decl(keyofInferenceIntersectsResults.ts, 9, 5))
38+
>foo : Symbol(foo, Decl(keyofInferenceIntersectsResults.ts, 3, 1))
39+
40+
const c = bar('a', 'b'); // still clean
41+
>c : Symbol(c, Decl(keyofInferenceIntersectsResults.ts, 10, 5))
42+
>bar : Symbol(bar, Decl(keyofInferenceIntersectsResults.ts, 5, 55))
43+
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
=== tests/cases/conformance/types/typeRelationships/typeInference/keyofInferenceIntersectsResults.ts ===
2+
interface X {
3+
>X : X
4+
5+
a: string;
6+
>a : string
7+
8+
b: string;
9+
>b : string
10+
}
11+
12+
declare function foo<T = X>(x: keyof T, y: keyof T): T;
13+
>foo : <T = X>(x: keyof T, y: keyof T) => T
14+
>T : T
15+
>X : X
16+
>x : keyof T
17+
>T : T
18+
>y : keyof T
19+
>T : T
20+
>T : T
21+
22+
declare function bar<T>(x: keyof T, y: keyof T): T;
23+
>bar : <T>(x: keyof T, y: keyof T) => T
24+
>T : T
25+
>x : keyof T
26+
>T : T
27+
>y : keyof T
28+
>T : T
29+
>T : T
30+
31+
const a = foo<X>('a', 'b'); // compiles cleanly
32+
>a : X
33+
>foo<X>('a', 'b') : X
34+
>foo : <T = X>(x: keyof T, y: keyof T) => T
35+
>X : X
36+
>'a' : "a"
37+
>'b' : "b"
38+
39+
const b = foo('a', 'b'); // also clean
40+
>b : { a: any; } & { b: any; }
41+
>foo('a', 'b') : { a: any; } & { b: any; }
42+
>foo : <T = X>(x: keyof T, y: keyof T) => T
43+
>'a' : "a"
44+
>'b' : "b"
45+
46+
const c = bar('a', 'b'); // still clean
47+
>c : { a: any; } & { b: any; }
48+
>bar('a', 'b') : { a: any; } & { b: any; }
49+
>bar : <T>(x: keyof T, y: keyof T) => T
50+
>'a' : "a"
51+
>'b' : "b"
52+
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
//// [keyofInferenceLowerPriorityThanReturn.ts]
2+
// #22736
3+
declare class Write {
4+
protected dummy: Write;
5+
}
6+
7+
declare class Col<s, a> {
8+
protected dummy: [Col<s, a>, s, a];
9+
}
10+
11+
declare class Table<Req, Def> {
12+
protected dummy: [Table<Req, Def>, Req, Def];
13+
}
14+
15+
type MakeTable<T1 extends object, T2 extends object> = {
16+
[P in keyof T1]: Col<Write, T1[P]>;
17+
} & {
18+
[P in keyof T2]: Col<Write, T2[P]>;
19+
};
20+
21+
declare class ConflictTarget<Cols> {
22+
public static tableColumns<Cols>(cols: (keyof Cols)[]): ConflictTarget<Cols>;
23+
protected dummy: [ConflictTarget<Cols>, Cols];
24+
}
25+
26+
27+
28+
const bookTable: Table<BookReq, BookDef> = null as any
29+
30+
interface BookReq {
31+
readonly title: string;
32+
readonly serial: number;
33+
}
34+
35+
interface BookDef {
36+
readonly author: string;
37+
readonly numPages: number | null;
38+
}
39+
40+
41+
function insertOnConflictDoNothing<Req extends object, Def extends object>(_table: Table<Req, Def>, _conflictTarget: ConflictTarget<Req & Def>): boolean {
42+
throw new Error();
43+
}
44+
45+
function f() {
46+
insertOnConflictDoNothing(bookTable, ConflictTarget.tableColumns(["serial"])); // <-- No error here; should use the type inferred for the return type of `tableColumns`
47+
}
48+
49+
50+
//// [keyofInferenceLowerPriorityThanReturn.js]
51+
var bookTable = null;
52+
function insertOnConflictDoNothing(_table, _conflictTarget) {
53+
throw new Error();
54+
}
55+
function f() {
56+
insertOnConflictDoNothing(bookTable, ConflictTarget.tableColumns(["serial"])); // <-- No error here; should use the type inferred for the return type of `tableColumns`
57+
}

0 commit comments

Comments
 (0)