Skip to content

Commit ba45252

Browse files
authored
More specific inference for constrained 'infer' types in template literal types (#48094)
* More specific inference for constrained 'infer' types in template literal types * PR feedback * Add inference priority for template type placeholders * Infer to a preferred constraint instead of a union * Add reduceType * Switch tests to use infer..extends * Add missing primitive constraint cases * Update .types tests * Remove TemplateTypePlaceholderPriority * Remove reduceType
1 parent 38631e6 commit ba45252

File tree

7 files changed

+2691
-7
lines changed

7 files changed

+2691
-7
lines changed

src/compiler/checker.ts

+87-6
Original file line numberDiff line numberDiff line change
@@ -22112,13 +22112,40 @@ namespace ts {
2211222112
sourceEnd.slice(sourceEnd.length - endLen) !== targetEnd.slice(targetEnd.length - endLen);
2211322113
}
2211422114

22115-
function isValidBigIntString(s: string): boolean {
22115+
/**
22116+
* Tests whether the provided string can be parsed as a number.
22117+
* @param s The string to test.
22118+
* @param roundTripOnly Indicates the resulting number matches the input when converted back to a string.
22119+
*/
22120+
function isValidNumberString(s: string, roundTripOnly: boolean): boolean {
22121+
if (s === "") return false;
22122+
const n = +s;
22123+
return isFinite(n) && (!roundTripOnly || "" + n === s);
22124+
}
22125+
22126+
/**
22127+
* @param text a valid bigint string excluding a trailing `n`, but including a possible prefix `-`. Use `isValidBigIntString(text, roundTripOnly)` before calling this function.
22128+
*/
22129+
function parseBigIntLiteralType(text: string) {
22130+
const negative = text.startsWith("-");
22131+
const base10Value = parsePseudoBigInt(`${negative ? text.slice(1) : text}n`);
22132+
return getBigIntLiteralType({ negative, base10Value });
22133+
}
22134+
22135+
/**
22136+
* Tests whether the provided string can be parsed as a bigint.
22137+
* @param s The string to test.
22138+
* @param roundTripOnly Indicates the resulting bigint matches the input when converted back to a string.
22139+
*/
22140+
function isValidBigIntString(s: string, roundTripOnly: boolean): boolean {
22141+
if (s === "") return false;
2211622142
const scanner = createScanner(ScriptTarget.ESNext, /*skipTrivia*/ false);
2211722143
let success = true;
2211822144
scanner.setOnError(() => success = false);
2211922145
scanner.setText(s + "n");
2212022146
let result = scanner.scan();
22121-
if (result === SyntaxKind.MinusToken) {
22147+
const negative = result === SyntaxKind.MinusToken;
22148+
if (negative) {
2212222149
result = scanner.scan();
2212322150
}
2212422151
const flags = scanner.getTokenFlags();
@@ -22127,7 +22154,8 @@ namespace ts {
2212722154
// * a bigint can be scanned, and that when it is scanned, it is
2212822155
// * the full length of the input string (so the scanner is one character beyond the augmented input length)
2212922156
// * it does not contain a numeric seperator (the `BigInt` constructor does not accept a numeric seperator in its input)
22130-
return success && result === SyntaxKind.BigIntLiteral && scanner.getTextPos() === (s.length + 1) && !(flags & TokenFlags.ContainsSeparator);
22157+
return success && result === SyntaxKind.BigIntLiteral && scanner.getTextPos() === (s.length + 1) && !(flags & TokenFlags.ContainsSeparator)
22158+
&& (!roundTripOnly || s === pseudoBigIntToString({ negative, base10Value: parsePseudoBigInt(scanner.getTokenValue()) }));
2213122159
}
2213222160

2213322161
function isValidTypeForTemplateLiteralPlaceholder(source: Type, target: Type): boolean {
@@ -22136,8 +22164,8 @@ namespace ts {
2213622164
}
2213722165
if (source.flags & TypeFlags.StringLiteral) {
2213822166
const value = (source as StringLiteralType).value;
22139-
return !!(target.flags & TypeFlags.Number && value !== "" && isFinite(+value) ||
22140-
target.flags & TypeFlags.BigInt && value !== "" && isValidBigIntString(value) ||
22167+
return !!(target.flags & TypeFlags.Number && isValidNumberString(value, /*roundTripOnly*/ false) ||
22168+
target.flags & TypeFlags.BigInt && isValidBigIntString(value, /*roundTripOnly*/ false) ||
2214122169
target.flags & (TypeFlags.BooleanLiteral | TypeFlags.Nullable) && value === (target as IntrinsicType).intrinsicName);
2214222170
}
2214322171
if (source.flags & TypeFlags.TemplateLiteral) {
@@ -22715,7 +22743,60 @@ namespace ts {
2271522743
// succeed. That would be a pointless and confusing outcome.
2271622744
if (matches || every(target.texts, s => s.length === 0)) {
2271722745
for (let i = 0; i < types.length; i++) {
22718-
inferFromTypes(matches ? matches[i] : neverType, types[i]);
22746+
const source = matches ? matches[i] : neverType;
22747+
const target = types[i];
22748+
22749+
// If we are inferring from a string literal type to a type variable whose constraint includes one of the
22750+
// allowed template literal placeholder types, infer from a literal type corresponding to the constraint.
22751+
if (source.flags & TypeFlags.StringLiteral && target.flags & TypeFlags.TypeVariable) {
22752+
const inferenceContext = getInferenceInfoForType(target);
22753+
const constraint = inferenceContext ? getBaseConstraintOfType(inferenceContext.typeParameter) : undefined;
22754+
if (constraint && !isTypeAny(constraint)) {
22755+
const constraintTypes = constraint.flags & TypeFlags.Union ? (constraint as UnionType).types : [constraint];
22756+
let allTypeFlags: TypeFlags = reduceLeft(constraintTypes, (flags, t) => flags | t.flags, 0 as TypeFlags);
22757+
22758+
// If the constraint contains `string`, we don't need to look for a more preferred type
22759+
if (!(allTypeFlags & TypeFlags.String)) {
22760+
const str = (source as StringLiteralType).value;
22761+
22762+
// If the type contains `number` or a number literal and the string isn't a valid number, exclude numbers
22763+
if (allTypeFlags & TypeFlags.NumberLike && !isValidNumberString(str, /*roundTripOnly*/ true)) {
22764+
allTypeFlags &= ~TypeFlags.NumberLike;
22765+
}
22766+
22767+
// If the type contains `bigint` or a bigint literal and the string isn't a valid bigint, exclude bigints
22768+
if (allTypeFlags & TypeFlags.BigIntLike && !isValidBigIntString(str, /*roundTripOnly*/ true)) {
22769+
allTypeFlags &= ~TypeFlags.BigIntLike;
22770+
}
22771+
22772+
// for each type in the constraint, find the highest priority matching type
22773+
const matchingType = reduceLeft(constraintTypes, (left, right) =>
22774+
!(right.flags & allTypeFlags) ? left :
22775+
left.flags & TypeFlags.String ? left : right.flags & TypeFlags.String ? source :
22776+
left.flags & TypeFlags.TemplateLiteral ? left : right.flags & TypeFlags.TemplateLiteral && isTypeMatchedByTemplateLiteralType(source, right as TemplateLiteralType) ? source :
22777+
left.flags & TypeFlags.StringMapping ? left : right.flags & TypeFlags.StringMapping && str === applyStringMapping(right.symbol, str) ? source :
22778+
left.flags & TypeFlags.StringLiteral ? left : right.flags & TypeFlags.StringLiteral && (right as StringLiteralType).value === str ? right :
22779+
left.flags & TypeFlags.Number ? left : right.flags & TypeFlags.Number ? getNumberLiteralType(+str) :
22780+
left.flags & TypeFlags.Enum ? left : right.flags & TypeFlags.Enum ? getNumberLiteralType(+str) :
22781+
left.flags & TypeFlags.NumberLiteral ? left : right.flags & TypeFlags.NumberLiteral && (right as NumberLiteralType).value === +str ? right :
22782+
left.flags & TypeFlags.BigInt ? left : right.flags & TypeFlags.BigInt ? parseBigIntLiteralType(str) :
22783+
left.flags & TypeFlags.BigIntLiteral ? left : right.flags & TypeFlags.BigIntLiteral && pseudoBigIntToString((right as BigIntLiteralType).value) === str ? right :
22784+
left.flags & TypeFlags.Boolean ? left : right.flags & TypeFlags.Boolean ? str === "true" ? trueType : str === "false" ? falseType : booleanType :
22785+
left.flags & TypeFlags.BooleanLiteral ? left : right.flags & TypeFlags.BooleanLiteral && (right as IntrinsicType).intrinsicName === str ? right :
22786+
left.flags & TypeFlags.Undefined ? left : right.flags & TypeFlags.Undefined && (right as IntrinsicType).intrinsicName === str ? right :
22787+
left.flags & TypeFlags.Null ? left : right.flags & TypeFlags.Null && (right as IntrinsicType).intrinsicName === str ? right :
22788+
left,
22789+
neverType as Type);
22790+
22791+
if (!(matchingType.flags & TypeFlags.Never)) {
22792+
inferFromTypes(matchingType, target);
22793+
continue;
22794+
}
22795+
}
22796+
}
22797+
}
22798+
22799+
inferFromTypes(source, target);
2271922800
}
2272022801
}
2272122802
}

src/compiler/types.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -5877,7 +5877,7 @@ namespace ts {
58775877
AlwaysStrict = 1 << 10, // Always use strict rules for contravariant inferences
58785878
MaxValue = 1 << 11, // Seed for inference priority tracking
58795879

5880-
PriorityImpliesCombination = ReturnType | MappedTypeConstraint | LiteralKeyof, // These priorities imply that the resulting type should be a combination of all candidates
5880+
PriorityImpliesCombination = ReturnType | MappedTypeConstraint | LiteralKeyof, // These priorities imply that the resulting type should be a combination of all candidates
58815881
Circularity = -1, // Inference circularity (value less than all other priorities)
58825882
}
58835883

0 commit comments

Comments
 (0)