Skip to content

Commit d544213

Browse files
committed
Infer to a preferred constraint instead of a union
1 parent df23e95 commit d544213

File tree

9 files changed

+708
-319
lines changed

9 files changed

+708
-319
lines changed

src/compiler/checker.ts

Lines changed: 76 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -261,6 +261,20 @@ namespace ts {
261261
VoidIsNonOptional = 1 << 1,
262262
}
263263

264+
const enum TemplateTypePlaceholderPriority {
265+
Never, // lowest
266+
Null,
267+
Undefined,
268+
BooleanLiterals,
269+
Boolean,
270+
BigIntLiterals,
271+
BigInt,
272+
NumberLiterals,
273+
Number,
274+
StringLiterals,
275+
String, // highest
276+
}
277+
264278
const enum IntrinsicTypeKind {
265279
Uppercase,
266280
Lowercase,
@@ -22382,6 +22396,20 @@ namespace ts {
2238222396
}
2238322397
}
2238422398

22399+
function getTemplateTypePlaceholderPriority(type: Type) {
22400+
return type.flags & TypeFlags.String ? TemplateTypePlaceholderPriority.String :
22401+
type.flags & TypeFlags.StringLiteral ? TemplateTypePlaceholderPriority.StringLiterals :
22402+
type.flags & TypeFlags.Number ? TemplateTypePlaceholderPriority.Number :
22403+
type.flags & TypeFlags.NumberLiteral ? TemplateTypePlaceholderPriority.NumberLiterals :
22404+
type.flags & TypeFlags.BigInt ? TemplateTypePlaceholderPriority.BigInt :
22405+
type.flags & TypeFlags.BigIntLiteral ? TemplateTypePlaceholderPriority.BigIntLiterals :
22406+
type.flags & TypeFlags.Boolean ? TemplateTypePlaceholderPriority.Boolean :
22407+
type.flags & TypeFlags.BooleanLiteral ? TemplateTypePlaceholderPriority.BooleanLiterals :
22408+
type.flags & TypeFlags.Undefined ? TemplateTypePlaceholderPriority.Undefined :
22409+
type.flags & TypeFlags.Null ? TemplateTypePlaceholderPriority.Null :
22410+
TemplateTypePlaceholderPriority.Never;
22411+
}
22412+
2238522413
function inferToTemplateLiteralType(source: Type, target: TemplateLiteralType) {
2238622414
const matches = inferTypesFromTemplateLiteralType(source, target);
2238722415
const types = target.types;
@@ -22398,42 +22426,61 @@ namespace ts {
2239822426

2239922427
// If we are inferring from a string literal type to a type variable whose constraint includes one of the
2240022428
// allowed template literal placeholder types, infer from a literal type corresponding to the constraint.
22401-
let sourceTypes: Type[] | undefined;
2240222429
if (source.flags & TypeFlags.StringLiteral && target.flags & TypeFlags.TypeVariable) {
2240322430
const inferenceContext = getInferenceInfoForType(target);
22404-
const constraint = inferenceContext ? getConstraintOfTypeParameter(inferenceContext.typeParameter) : undefined;
22405-
if (constraint) {
22406-
const str = (source as StringLiteralType).value;
22407-
const constraintTypes = constraint.flags & TypeFlags.Union ? (constraint as UnionType).types : [constraint];
22408-
for (const constraintType of constraintTypes) {
22409-
const sourceType =
22410-
constraintType.flags & TypeFlags.StringLike ? source :
22411-
constraintType.flags & TypeFlags.NumberLike && isValidNumberString(str, /*roundTripOnly*/ true) ? getNumberLiteralType(+str) :
22412-
constraintType.flags & TypeFlags.BigIntLike && isValidBigIntString(str, /*roundTripOnly*/ true) ? parseBigIntLiteralType(str) :
22413-
constraintType.flags & TypeFlags.BooleanLike && str === trueType.intrinsicName ? trueType :
22414-
constraintType.flags & TypeFlags.BooleanLike && str === falseType.intrinsicName ? falseType :
22415-
constraintType.flags & TypeFlags.Null && str === nullType.intrinsicName ? nullType :
22416-
constraintType.flags & TypeFlags.Undefined && str === undefinedType.intrinsicName ? undefinedType :
22417-
undefined;
22418-
if (sourceType) {
22419-
sourceTypes ??= [];
22420-
sourceTypes.push(sourceType);
22431+
const constraint = inferenceContext ? getBaseConstraintOfType(inferenceContext.typeParameter) : undefined;
22432+
if (constraint && !isTypeAny(constraint)) {
22433+
let allTypeFlags: TypeFlags = 0;
22434+
forEachType(constraint, t => { allTypeFlags |= t.flags; });
22435+
22436+
// If the constraint contains `string`, we don't need to look for a more preferred type
22437+
if (!(allTypeFlags & TypeFlags.String)) {
22438+
const str = (source as StringLiteralType).value;
22439+
22440+
// If the type contains `number` or a number literal and the string isn't a valid number, exclude numbers
22441+
if (allTypeFlags & TypeFlags.NumberLike && !isValidNumberString(str, /*roundTripOnly*/ true)) {
22442+
allTypeFlags &= ~TypeFlags.NumberLike;
22443+
}
22444+
22445+
// If the type contains `bigint` or a bigint literal and the string isn't a valid bigint, exclude bigints
22446+
if (allTypeFlags & TypeFlags.BigIntLike && !isValidBigIntString(str, /*roundTripOnly*/ true)) {
22447+
allTypeFlags &= ~TypeFlags.BigIntLike;
22448+
}
22449+
22450+
// for each type in the constraint, find the highest priority matching type
22451+
let matchingType: Type | undefined;
22452+
let matchingTypePriority = TemplateTypePlaceholderPriority.Never;
22453+
forEachType(constraint, t => {
22454+
if (t.flags & allTypeFlags) {
22455+
const typePriority = getTemplateTypePlaceholderPriority(t);
22456+
if (typePriority > matchingTypePriority) {
22457+
const newMatchingType =
22458+
t.flags & TypeFlags.String ? source :
22459+
t.flags & TypeFlags.Number ? getNumberLiteralType(+str) : // if `str` was not a valid number, TypeFlags.Number would have been excluded above.
22460+
t.flags & TypeFlags.BigInt ? parseBigIntLiteralType(str) : // if `str` was not a valid bigint, TypeFlags.BigInt would have been excluded above.
22461+
t.flags & TypeFlags.Boolean ? str === "true" ? trueType : falseType :
22462+
t.flags & TypeFlags.StringLiteral && (t as StringLiteralType).value === str ? t :
22463+
t.flags & TypeFlags.NumberLiteral && (t as NumberLiteralType).value === +str ? t :
22464+
t.flags & TypeFlags.BigIntLiteral && pseudoBigIntToString((t as BigIntLiteralType).value) === str ? t :
22465+
t.flags & (TypeFlags.BooleanLiteral | TypeFlags.Nullable) && (t as IntrinsicType).intrinsicName === str ? t :
22466+
undefined;
22467+
if (newMatchingType) {
22468+
matchingType = newMatchingType;
22469+
matchingTypePriority = typePriority;
22470+
}
22471+
}
22472+
}
22473+
});
22474+
22475+
if (matchingType) {
22476+
inferFromTypes(matchingType, target);
22477+
continue;
2242122478
}
2242222479
}
2242322480
}
2242422481
}
2242522482

22426-
if (sourceTypes) {
22427-
const savedPriority = priority;
22428-
priority |= InferencePriority.TemplateLiteralPlaceholder;
22429-
for (const source of sourceTypes) {
22430-
inferFromTypes(source, target);
22431-
}
22432-
priority = savedPriority;
22433-
}
22434-
else {
22435-
inferFromTypes(source, target);
22436-
}
22483+
inferFromTypes(source, target);
2243722484
}
2243822485
}
2243922486
}

src/compiler/types.ts

Lines changed: 13 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -5825,20 +5825,19 @@ namespace ts {
58255825

58265826
export const enum InferencePriority {
58275827
NakedTypeVariable = 1 << 0, // Naked type variable in union or intersection type
5828-
TemplateLiteralPlaceholder = 1 << 1, // Inference to a template literal type placeholder
5829-
SpeculativeTuple = 1 << 2, // Speculative tuple inference
5830-
SubstituteSource = 1 << 3, // Source of inference originated within a substitution type's substitute
5831-
HomomorphicMappedType = 1 << 4, // Reverse inference for homomorphic mapped type
5832-
PartialHomomorphicMappedType = 1 << 5, // Partial reverse inference for homomorphic mapped type
5833-
MappedTypeConstraint = 1 << 6, // Reverse inference for mapped type
5834-
ContravariantConditional = 1 << 7, // Conditional type in contravariant position
5835-
ReturnType = 1 << 8, // Inference made from return type of generic function
5836-
LiteralKeyof = 1 << 9, // Inference made from a string literal to a keyof T
5837-
NoConstraints = 1 << 10, // Don't infer from constraints of instantiable types
5838-
AlwaysStrict = 1 << 11, // Always use strict rules for contravariant inferences
5839-
MaxValue = 1 << 12, // Seed for inference priority tracking
5840-
5841-
PriorityImpliesCombination = ReturnType | MappedTypeConstraint | LiteralKeyof | TemplateLiteralPlaceholder, // These priorities imply that the resulting type should be a combination of all candidates
5828+
SpeculativeTuple = 1 << 1, // Speculative tuple inference
5829+
SubstituteSource = 1 << 2, // Source of inference originated within a substitution type's substitute
5830+
HomomorphicMappedType = 1 << 3, // Reverse inference for homomorphic mapped type
5831+
PartialHomomorphicMappedType = 1 << 4, // Partial reverse inference for homomorphic mapped type
5832+
MappedTypeConstraint = 1 << 5, // Reverse inference for mapped type
5833+
ContravariantConditional = 1 << 6, // Conditional type in contravariant position
5834+
ReturnType = 1 << 7, // Inference made from return type of generic function
5835+
LiteralKeyof = 1 << 8, // Inference made from a string literal to a keyof T
5836+
NoConstraints = 1 << 9, // Don't infer from constraints of instantiable types
5837+
AlwaysStrict = 1 << 10, // Always use strict rules for contravariant inferences
5838+
MaxValue = 1 << 11, // Seed for inference priority tracking
5839+
5840+
PriorityImpliesCombination = ReturnType | MappedTypeConstraint | LiteralKeyof, // These priorities imply that the resulting type should be a combination of all candidates
58425841
Circularity = -1, // Inference circularity (value less than all other priorities)
58435842
}
58445843

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

Lines changed: 12 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -2805,19 +2805,18 @@ declare namespace ts {
28052805
}
28062806
export enum InferencePriority {
28072807
NakedTypeVariable = 1,
2808-
TemplateLiteralPlaceholder = 2,
2809-
SpeculativeTuple = 4,
2810-
SubstituteSource = 8,
2811-
HomomorphicMappedType = 16,
2812-
PartialHomomorphicMappedType = 32,
2813-
MappedTypeConstraint = 64,
2814-
ContravariantConditional = 128,
2815-
ReturnType = 256,
2816-
LiteralKeyof = 512,
2817-
NoConstraints = 1024,
2818-
AlwaysStrict = 2048,
2819-
MaxValue = 4096,
2820-
PriorityImpliesCombination = 834,
2808+
SpeculativeTuple = 2,
2809+
SubstituteSource = 4,
2810+
HomomorphicMappedType = 8,
2811+
PartialHomomorphicMappedType = 16,
2812+
MappedTypeConstraint = 32,
2813+
ContravariantConditional = 64,
2814+
ReturnType = 128,
2815+
LiteralKeyof = 256,
2816+
NoConstraints = 512,
2817+
AlwaysStrict = 1024,
2818+
MaxValue = 2048,
2819+
PriorityImpliesCombination = 416,
28212820
Circularity = -1
28222821
}
28232822
/** @deprecated Use FileExtensionInfo instead. */

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

Lines changed: 12 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -2805,19 +2805,18 @@ declare namespace ts {
28052805
}
28062806
export enum InferencePriority {
28072807
NakedTypeVariable = 1,
2808-
TemplateLiteralPlaceholder = 2,
2809-
SpeculativeTuple = 4,
2810-
SubstituteSource = 8,
2811-
HomomorphicMappedType = 16,
2812-
PartialHomomorphicMappedType = 32,
2813-
MappedTypeConstraint = 64,
2814-
ContravariantConditional = 128,
2815-
ReturnType = 256,
2816-
LiteralKeyof = 512,
2817-
NoConstraints = 1024,
2818-
AlwaysStrict = 2048,
2819-
MaxValue = 4096,
2820-
PriorityImpliesCombination = 834,
2808+
SpeculativeTuple = 2,
2809+
SubstituteSource = 4,
2810+
HomomorphicMappedType = 8,
2811+
PartialHomomorphicMappedType = 16,
2812+
MappedTypeConstraint = 32,
2813+
ContravariantConditional = 64,
2814+
ReturnType = 128,
2815+
LiteralKeyof = 256,
2816+
NoConstraints = 512,
2817+
AlwaysStrict = 1024,
2818+
MaxValue = 2048,
2819+
PriorityImpliesCombination = 416,
28212820
Circularity = -1
28222821
}
28232822
/** @deprecated Use FileExtensionInfo instead. */

tests/baselines/reference/templateLiteralTypes4.errors.txt

Lines changed: 52 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
1-
tests/cases/conformance/types/literal/templateLiteralTypes4.ts(93,12): error TS2345: Argument of type '2' is not assignable to parameter of type '0 | 1'.
2-
tests/cases/conformance/types/literal/templateLiteralTypes4.ts(97,12): error TS2345: Argument of type '2' is not assignable to parameter of type '0 | 1'.
1+
tests/cases/conformance/types/literal/templateLiteralTypes4.ts(128,12): error TS2345: Argument of type '2' is not assignable to parameter of type '0 | 1'.
2+
tests/cases/conformance/types/literal/templateLiteralTypes4.ts(132,12): error TS2345: Argument of type '2' is not assignable to parameter of type '0 | 1'.
33

44

55
==== tests/cases/conformance/types/literal/templateLiteralTypes4.ts (2 errors) ====
@@ -34,12 +34,47 @@ tests/cases/conformance/types/literal/templateLiteralTypes4.ts(97,12): error TS2
3434
type T40 = "undefined" extends `${Is<infer T, undefined>}` ? T : never; // undefined
3535
type T41 = "abcd" extends `${Is<infer T, undefined>}` ? T : never; // never
3636

37-
type T50 = "100" extends `${Is<infer T, string | number | bigint | boolean | null | undefined>}` ? T : never; // "100" | 100 | 100n
38-
type T51 = "1.1" extends `${Is<infer T, string | number | bigint | boolean | null | undefined>}` ? T : never; // "100" | 1.1
39-
type T52 = "true" extends `${Is<infer T, string | number | bigint | boolean | null | undefined>}` ? T : never; // "true" | true
40-
type T53 = "false" extends `${Is<infer T, string | number | bigint | boolean | null | undefined>}` ? T : never; // "false" | false
41-
type T54 = "null" extends `${Is<infer T, string | number | bigint | boolean | null | undefined>}` ? T : never; // "null" | null
42-
type T55 = "undefined" extends `${Is<infer T, string | number | bigint | boolean | null | undefined>}` ? T : never; // "undefined" | undefined
37+
type T500 = "100" extends `${Is<infer T, string | number | bigint>}` ? T : never; // "100"
38+
type T501 = "100" extends `${Is<infer T, number | bigint>}` ? T : never; // 100
39+
type T502 = "100" extends `${Is<infer T, bigint>}` ? T : never; // 100n
40+
type T503 = "100" extends `${Is<infer T, "100" | number>}` ? T : never; // "100"
41+
type T504 = "100" extends `${Is<infer T, "101" | number>}` ? T : never; // 100
42+
43+
type T510 = "1.1" extends `${Is<infer T, string | number | bigint>}` ? T : never; // "1.1"
44+
type T511 = "1.1" extends `${Is<infer T, number | bigint>}` ? T : never; // 1.1
45+
type T512 = "1.1" extends `${Is<infer T, bigint>}` ? T : never; // never
46+
47+
type T520 = "true" extends `${Is<infer T, string | boolean>}` ? T : never; // "true"
48+
type T521 = "true" extends `${Is<infer T, boolean>}` ? T : never; // true
49+
50+
type T530 = "false" extends `${Is<infer T, string | boolean>}` ? T : never; // "false"
51+
type T531 = "false" extends `${Is<infer T, boolean>}` ? T : never; // false
52+
53+
type T540 = "null" extends `${Is<infer T, string | null>}` ? T : never; // "null"
54+
type T541 = "null" extends `${Is<infer T, string | null>}` ? T : never; // null
55+
56+
type T550 = "undefined" extends `${Is<infer T, string | undefined>}` ? T : never; // "undefined"
57+
type T551 = "undefined" extends `${Is<infer T, undefined>}` ? T : never; // undefined
58+
59+
type T560 = "100000000000000000000000" extends `${Is<infer T, number | bigint>}` ? T : never; // 100000000000000000000000n
60+
type T561 = "100000000000000000000000" extends `${Is<infer T, number>}` ? T : never; // number
61+
62+
type ExtractPrimitives<T extends string> =
63+
| T
64+
| (T extends `${Is<infer U, number>}` ? U : never)
65+
| (T extends `${Is<infer U, bigint>}` ? U : never)
66+
| (T extends `${Is<infer U, boolean | null | undefined>}` ? U : never)
67+
;
68+
69+
// Type writer doesn't show the union that is produced, so we use a helper type to verify constraints
70+
type T570 = ExtractPrimitives<"100">;
71+
type CheckT570 = Is<"100" | 100 | 100n, T570>;
72+
73+
type T571 = ExtractPrimitives<"1.1">;
74+
type CheckT571 = Is<"1.1" | 1.1, T571>;
75+
76+
type T572 = ExtractPrimitives<"true">;
77+
type CheckT572 = Is<"true" | true, T572>;
4378

4479
type NumberFor<S extends string> = S extends `${Is<infer N, number>}` ? N : never;
4580
type T60 = NumberFor<"100">; // 100
@@ -106,12 +141,15 @@ tests/cases/conformance/types/literal/templateLiteralTypes4.ts(97,12): error TS2
106141
!!! error TS2345: Argument of type '2' is not assignable to parameter of type '0 | 1'.
107142

108143
declare function f1<T extends string | number>(s: `**${T}**`): T;
109-
f1("**123**"); // "123" | 123
144+
f1("**123**"); // "123"
145+
146+
declare function f2<T extends number>(s: `**${T}**`): T;
147+
f2("**123**"); // 123
110148

111-
declare function f2<T extends string | bigint>(s: `**${T}**`): T;
112-
f2("**123**"); // "123" | 123n
149+
declare function f3<T extends bigint>(s: `**${T}**`): T;
150+
f3("**123**"); // 123n
113151

114-
declare function f3<T extends string | boolean>(s: `**${T}**`): T;
115-
f3("**true**"); // true | "true"
116-
f3("**false**"); // false | "false"
152+
declare function f4<T extends boolean>(s: `**${T}**`): T;
153+
f4("**true**"); // true | "true"
154+
f4("**false**"); // false | "false"
117155

0 commit comments

Comments
 (0)