Skip to content

Commit b897272

Browse files
committed
Infer to a preferred constraint instead of a union
1 parent 55d4c3d commit b897272

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
@@ -263,6 +263,20 @@ namespace ts {
263263
VoidIsNonOptional = 1 << 1,
264264
}
265265

266+
const enum TemplateTypePlaceholderPriority {
267+
Never, // lowest
268+
Null,
269+
Undefined,
270+
BooleanLiterals,
271+
Boolean,
272+
BigIntLiterals,
273+
BigInt,
274+
NumberLiterals,
275+
Number,
276+
StringLiterals,
277+
String, // highest
278+
}
279+
266280
const enum IntrinsicTypeKind {
267281
Uppercase,
268282
Lowercase,
@@ -22729,6 +22743,20 @@ namespace ts {
2272922743
}
2273022744
}
2273122745

22746+
function getTemplateTypePlaceholderPriority(type: Type) {
22747+
return type.flags & TypeFlags.String ? TemplateTypePlaceholderPriority.String :
22748+
type.flags & TypeFlags.StringLiteral ? TemplateTypePlaceholderPriority.StringLiterals :
22749+
type.flags & TypeFlags.Number ? TemplateTypePlaceholderPriority.Number :
22750+
type.flags & TypeFlags.NumberLiteral ? TemplateTypePlaceholderPriority.NumberLiterals :
22751+
type.flags & TypeFlags.BigInt ? TemplateTypePlaceholderPriority.BigInt :
22752+
type.flags & TypeFlags.BigIntLiteral ? TemplateTypePlaceholderPriority.BigIntLiterals :
22753+
type.flags & TypeFlags.Boolean ? TemplateTypePlaceholderPriority.Boolean :
22754+
type.flags & TypeFlags.BooleanLiteral ? TemplateTypePlaceholderPriority.BooleanLiterals :
22755+
type.flags & TypeFlags.Undefined ? TemplateTypePlaceholderPriority.Undefined :
22756+
type.flags & TypeFlags.Null ? TemplateTypePlaceholderPriority.Null :
22757+
TemplateTypePlaceholderPriority.Never;
22758+
}
22759+
2273222760
function inferToTemplateLiteralType(source: Type, target: TemplateLiteralType) {
2273322761
const matches = inferTypesFromTemplateLiteralType(source, target);
2273422762
const types = target.types;
@@ -22745,42 +22773,61 @@ namespace ts {
2274522773

2274622774
// If we are inferring from a string literal type to a type variable whose constraint includes one of the
2274722775
// allowed template literal placeholder types, infer from a literal type corresponding to the constraint.
22748-
let sourceTypes: Type[] | undefined;
2274922776
if (source.flags & TypeFlags.StringLiteral && target.flags & TypeFlags.TypeVariable) {
2275022777
const inferenceContext = getInferenceInfoForType(target);
22751-
const constraint = inferenceContext ? getConstraintOfTypeParameter(inferenceContext.typeParameter) : undefined;
22752-
if (constraint) {
22753-
const str = (source as StringLiteralType).value;
22754-
const constraintTypes = constraint.flags & TypeFlags.Union ? (constraint as UnionType).types : [constraint];
22755-
for (const constraintType of constraintTypes) {
22756-
const sourceType =
22757-
constraintType.flags & TypeFlags.StringLike ? source :
22758-
constraintType.flags & TypeFlags.NumberLike && isValidNumberString(str, /*roundTripOnly*/ true) ? getNumberLiteralType(+str) :
22759-
constraintType.flags & TypeFlags.BigIntLike && isValidBigIntString(str, /*roundTripOnly*/ true) ? parseBigIntLiteralType(str) :
22760-
constraintType.flags & TypeFlags.BooleanLike && str === trueType.intrinsicName ? trueType :
22761-
constraintType.flags & TypeFlags.BooleanLike && str === falseType.intrinsicName ? falseType :
22762-
constraintType.flags & TypeFlags.Null && str === nullType.intrinsicName ? nullType :
22763-
constraintType.flags & TypeFlags.Undefined && str === undefinedType.intrinsicName ? undefinedType :
22764-
undefined;
22765-
if (sourceType) {
22766-
sourceTypes ??= [];
22767-
sourceTypes.push(sourceType);
22778+
const constraint = inferenceContext ? getBaseConstraintOfType(inferenceContext.typeParameter) : undefined;
22779+
if (constraint && !isTypeAny(constraint)) {
22780+
let allTypeFlags: TypeFlags = 0;
22781+
forEachType(constraint, t => { allTypeFlags |= t.flags; });
22782+
22783+
// If the constraint contains `string`, we don't need to look for a more preferred type
22784+
if (!(allTypeFlags & TypeFlags.String)) {
22785+
const str = (source as StringLiteralType).value;
22786+
22787+
// If the type contains `number` or a number literal and the string isn't a valid number, exclude numbers
22788+
if (allTypeFlags & TypeFlags.NumberLike && !isValidNumberString(str, /*roundTripOnly*/ true)) {
22789+
allTypeFlags &= ~TypeFlags.NumberLike;
22790+
}
22791+
22792+
// If the type contains `bigint` or a bigint literal and the string isn't a valid bigint, exclude bigints
22793+
if (allTypeFlags & TypeFlags.BigIntLike && !isValidBigIntString(str, /*roundTripOnly*/ true)) {
22794+
allTypeFlags &= ~TypeFlags.BigIntLike;
22795+
}
22796+
22797+
// for each type in the constraint, find the highest priority matching type
22798+
let matchingType: Type | undefined;
22799+
let matchingTypePriority = TemplateTypePlaceholderPriority.Never;
22800+
forEachType(constraint, t => {
22801+
if (t.flags & allTypeFlags) {
22802+
const typePriority = getTemplateTypePlaceholderPriority(t);
22803+
if (typePriority > matchingTypePriority) {
22804+
const newMatchingType =
22805+
t.flags & TypeFlags.String ? source :
22806+
t.flags & TypeFlags.Number ? getNumberLiteralType(+str) : // if `str` was not a valid number, TypeFlags.Number would have been excluded above.
22807+
t.flags & TypeFlags.BigInt ? parseBigIntLiteralType(str) : // if `str` was not a valid bigint, TypeFlags.BigInt would have been excluded above.
22808+
t.flags & TypeFlags.Boolean ? str === "true" ? trueType : falseType :
22809+
t.flags & TypeFlags.StringLiteral && (t as StringLiteralType).value === str ? t :
22810+
t.flags & TypeFlags.NumberLiteral && (t as NumberLiteralType).value === +str ? t :
22811+
t.flags & TypeFlags.BigIntLiteral && pseudoBigIntToString((t as BigIntLiteralType).value) === str ? t :
22812+
t.flags & (TypeFlags.BooleanLiteral | TypeFlags.Nullable) && (t as IntrinsicType).intrinsicName === str ? t :
22813+
undefined;
22814+
if (newMatchingType) {
22815+
matchingType = newMatchingType;
22816+
matchingTypePriority = typePriority;
22817+
}
22818+
}
22819+
}
22820+
});
22821+
22822+
if (matchingType) {
22823+
inferFromTypes(matchingType, target);
22824+
continue;
2276822825
}
2276922826
}
2277022827
}
2277122828
}
2277222829

22773-
if (sourceTypes) {
22774-
const savedPriority = priority;
22775-
priority |= InferencePriority.TemplateLiteralPlaceholder;
22776-
for (const source of sourceTypes) {
22777-
inferFromTypes(source, target);
22778-
}
22779-
priority = savedPriority;
22780-
}
22781-
else {
22782-
inferFromTypes(source, target);
22783-
}
22830+
inferFromTypes(source, target);
2278422831
}
2278522832
}
2278622833
}

src/compiler/types.ts

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

58595859
export const enum InferencePriority {
58605860
NakedTypeVariable = 1 << 0, // Naked type variable in union or intersection type
5861-
TemplateLiteralPlaceholder = 1 << 1, // Inference to a template literal type placeholder
5862-
SpeculativeTuple = 1 << 2, // Speculative tuple inference
5863-
SubstituteSource = 1 << 3, // Source of inference originated within a substitution type's substitute
5864-
HomomorphicMappedType = 1 << 4, // Reverse inference for homomorphic mapped type
5865-
PartialHomomorphicMappedType = 1 << 5, // Partial reverse inference for homomorphic mapped type
5866-
MappedTypeConstraint = 1 << 6, // Reverse inference for mapped type
5867-
ContravariantConditional = 1 << 7, // Conditional type in contravariant position
5868-
ReturnType = 1 << 8, // Inference made from return type of generic function
5869-
LiteralKeyof = 1 << 9, // Inference made from a string literal to a keyof T
5870-
NoConstraints = 1 << 10, // Don't infer from constraints of instantiable types
5871-
AlwaysStrict = 1 << 11, // Always use strict rules for contravariant inferences
5872-
MaxValue = 1 << 12, // Seed for inference priority tracking
5873-
5874-
PriorityImpliesCombination = ReturnType | MappedTypeConstraint | LiteralKeyof | TemplateLiteralPlaceholder, // These priorities imply that the resulting type should be a combination of all candidates
5861+
SpeculativeTuple = 1 << 1, // Speculative tuple inference
5862+
SubstituteSource = 1 << 2, // Source of inference originated within a substitution type's substitute
5863+
HomomorphicMappedType = 1 << 3, // Reverse inference for homomorphic mapped type
5864+
PartialHomomorphicMappedType = 1 << 4, // Partial reverse inference for homomorphic mapped type
5865+
MappedTypeConstraint = 1 << 5, // Reverse inference for mapped type
5866+
ContravariantConditional = 1 << 6, // Conditional type in contravariant position
5867+
ReturnType = 1 << 7, // Inference made from return type of generic function
5868+
LiteralKeyof = 1 << 8, // Inference made from a string literal to a keyof T
5869+
NoConstraints = 1 << 9, // Don't infer from constraints of instantiable types
5870+
AlwaysStrict = 1 << 10, // Always use strict rules for contravariant inferences
5871+
MaxValue = 1 << 11, // Seed for inference priority tracking
5872+
5873+
PriorityImpliesCombination = ReturnType | MappedTypeConstraint | LiteralKeyof, // These priorities imply that the resulting type should be a combination of all candidates
58755874
Circularity = -1, // Inference circularity (value less than all other priorities)
58765875
}
58775876

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

Lines changed: 12 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -2823,19 +2823,18 @@ declare namespace ts {
28232823
}
28242824
export enum InferencePriority {
28252825
NakedTypeVariable = 1,
2826-
TemplateLiteralPlaceholder = 2,
2827-
SpeculativeTuple = 4,
2828-
SubstituteSource = 8,
2829-
HomomorphicMappedType = 16,
2830-
PartialHomomorphicMappedType = 32,
2831-
MappedTypeConstraint = 64,
2832-
ContravariantConditional = 128,
2833-
ReturnType = 256,
2834-
LiteralKeyof = 512,
2835-
NoConstraints = 1024,
2836-
AlwaysStrict = 2048,
2837-
MaxValue = 4096,
2838-
PriorityImpliesCombination = 834,
2826+
SpeculativeTuple = 2,
2827+
SubstituteSource = 4,
2828+
HomomorphicMappedType = 8,
2829+
PartialHomomorphicMappedType = 16,
2830+
MappedTypeConstraint = 32,
2831+
ContravariantConditional = 64,
2832+
ReturnType = 128,
2833+
LiteralKeyof = 256,
2834+
NoConstraints = 512,
2835+
AlwaysStrict = 1024,
2836+
MaxValue = 2048,
2837+
PriorityImpliesCombination = 416,
28392838
Circularity = -1
28402839
}
28412840
/** @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
@@ -2823,19 +2823,18 @@ declare namespace ts {
28232823
}
28242824
export enum InferencePriority {
28252825
NakedTypeVariable = 1,
2826-
TemplateLiteralPlaceholder = 2,
2827-
SpeculativeTuple = 4,
2828-
SubstituteSource = 8,
2829-
HomomorphicMappedType = 16,
2830-
PartialHomomorphicMappedType = 32,
2831-
MappedTypeConstraint = 64,
2832-
ContravariantConditional = 128,
2833-
ReturnType = 256,
2834-
LiteralKeyof = 512,
2835-
NoConstraints = 1024,
2836-
AlwaysStrict = 2048,
2837-
MaxValue = 4096,
2838-
PriorityImpliesCombination = 834,
2826+
SpeculativeTuple = 2,
2827+
SubstituteSource = 4,
2828+
HomomorphicMappedType = 8,
2829+
PartialHomomorphicMappedType = 16,
2830+
MappedTypeConstraint = 32,
2831+
ContravariantConditional = 64,
2832+
ReturnType = 128,
2833+
LiteralKeyof = 256,
2834+
NoConstraints = 512,
2835+
AlwaysStrict = 1024,
2836+
MaxValue = 2048,
2837+
PriorityImpliesCombination = 416,
28392838
Circularity = -1
28402839
}
28412840
/** @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)