Skip to content
93 changes: 87 additions & 6 deletions src/compiler/checker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22109,13 +22109,40 @@ namespace ts {
sourceEnd.slice(sourceEnd.length - endLen) !== targetEnd.slice(targetEnd.length - endLen);
}

function isValidBigIntString(s: string): boolean {
/**
* Tests whether the provided string can be parsed as a number.
* @param s The string to test.
* @param roundTripOnly Indicates the resulting number matches the input when converted back to a string.
*/
function isValidNumberString(s: string, roundTripOnly: boolean): boolean {
if (s === "") return false;
const n = +s;
return isFinite(n) && (!roundTripOnly || "" + n === s);
}

/**
* @param text a valid bigint string excluding a trailing `n`, but including a possible prefix `-`. Use `isValidBigIntString(text, roundTripOnly)` before calling this function.
*/
function parseBigIntLiteralType(text: string) {
const negative = text.startsWith("-");
const base10Value = parsePseudoBigInt(`${negative ? text.slice(1) : text}n`);
return getBigIntLiteralType({ negative, base10Value });
}

/**
* Tests whether the provided string can be parsed as a bigint.
* @param s The string to test.
* @param roundTripOnly Indicates the resulting bigint matches the input when converted back to a string.
*/
function isValidBigIntString(s: string, roundTripOnly: boolean): boolean {
if (s === "") return false;
const scanner = createScanner(ScriptTarget.ESNext, /*skipTrivia*/ false);
let success = true;
scanner.setOnError(() => success = false);
scanner.setText(s + "n");
let result = scanner.scan();
if (result === SyntaxKind.MinusToken) {
const negative = result === SyntaxKind.MinusToken;
if (negative) {
result = scanner.scan();
}
const flags = scanner.getTokenFlags();
Expand All @@ -22124,7 +22151,8 @@ namespace ts {
// * a bigint can be scanned, and that when it is scanned, it is
// * the full length of the input string (so the scanner is one character beyond the augmented input length)
// * it does not contain a numeric seperator (the `BigInt` constructor does not accept a numeric seperator in its input)
return success && result === SyntaxKind.BigIntLiteral && scanner.getTextPos() === (s.length + 1) && !(flags & TokenFlags.ContainsSeparator);
return success && result === SyntaxKind.BigIntLiteral && scanner.getTextPos() === (s.length + 1) && !(flags & TokenFlags.ContainsSeparator)
&& (!roundTripOnly || s === pseudoBigIntToString({ negative, base10Value: parsePseudoBigInt(scanner.getTokenValue()) }));
}

function isValidTypeForTemplateLiteralPlaceholder(source: Type, target: Type): boolean {
Expand All @@ -22133,8 +22161,8 @@ namespace ts {
}
if (source.flags & TypeFlags.StringLiteral) {
const value = (source as StringLiteralType).value;
return !!(target.flags & TypeFlags.Number && value !== "" && isFinite(+value) ||
target.flags & TypeFlags.BigInt && value !== "" && isValidBigIntString(value) ||
return !!(target.flags & TypeFlags.Number && isValidNumberString(value, /*roundTripOnly*/ false) ||
target.flags & TypeFlags.BigInt && isValidBigIntString(value, /*roundTripOnly*/ false) ||
target.flags & (TypeFlags.BooleanLiteral | TypeFlags.Nullable) && value === (target as IntrinsicType).intrinsicName);
}
if (source.flags & TypeFlags.TemplateLiteral) {
Expand Down Expand Up @@ -22712,7 +22740,60 @@ namespace ts {
// succeed. That would be a pointless and confusing outcome.
if (matches || every(target.texts, s => s.length === 0)) {
for (let i = 0; i < types.length; i++) {
inferFromTypes(matches ? matches[i] : neverType, types[i]);
const source = matches ? matches[i] : neverType;
const target = types[i];

// If we are inferring from a string literal type to a type variable whose constraint includes one of the
// allowed template literal placeholder types, infer from a literal type corresponding to the constraint.
if (source.flags & TypeFlags.StringLiteral && target.flags & TypeFlags.TypeVariable) {
const inferenceContext = getInferenceInfoForType(target);
const constraint = inferenceContext ? getBaseConstraintOfType(inferenceContext.typeParameter) : undefined;
if (constraint && !isTypeAny(constraint)) {
const constraintTypes = constraint.flags & TypeFlags.Union ? (constraint as UnionType).types : [constraint];
let allTypeFlags: TypeFlags = reduceLeft(constraintTypes, (flags, t) => flags | t.flags, 0 as TypeFlags);

// If the constraint contains `string`, we don't need to look for a more preferred type
if (!(allTypeFlags & TypeFlags.String)) {
const str = (source as StringLiteralType).value;

// If the type contains `number` or a number literal and the string isn't a valid number, exclude numbers
if (allTypeFlags & TypeFlags.NumberLike && !isValidNumberString(str, /*roundTripOnly*/ true)) {
allTypeFlags &= ~TypeFlags.NumberLike;
}

// If the type contains `bigint` or a bigint literal and the string isn't a valid bigint, exclude bigints
if (allTypeFlags & TypeFlags.BigIntLike && !isValidBigIntString(str, /*roundTripOnly*/ true)) {
allTypeFlags &= ~TypeFlags.BigIntLike;
}

// for each type in the constraint, find the highest priority matching type
const matchingType = reduceLeft(constraintTypes, (left, right) =>
!(right.flags & allTypeFlags) ? left :
left.flags & TypeFlags.String ? left : right.flags & TypeFlags.String ? source :
left.flags & TypeFlags.TemplateLiteral ? left : right.flags & TypeFlags.TemplateLiteral && isTypeMatchedByTemplateLiteralType(source, right as TemplateLiteralType) ? source :
left.flags & TypeFlags.StringMapping ? left : right.flags & TypeFlags.StringMapping && str === applyStringMapping(right.symbol, str) ? source :
left.flags & TypeFlags.StringLiteral ? left : right.flags & TypeFlags.StringLiteral && (right as StringLiteralType).value === str ? right :
left.flags & TypeFlags.Number ? left : right.flags & TypeFlags.Number ? getNumberLiteralType(+str) :
left.flags & TypeFlags.Enum ? left : right.flags & TypeFlags.Enum ? getNumberLiteralType(+str) :
left.flags & TypeFlags.NumberLiteral ? left : right.flags & TypeFlags.NumberLiteral && (right as NumberLiteralType).value === +str ? right :
left.flags & TypeFlags.BigInt ? left : right.flags & TypeFlags.BigInt ? parseBigIntLiteralType(str) :
left.flags & TypeFlags.BigIntLiteral ? left : right.flags & TypeFlags.BigIntLiteral && pseudoBigIntToString((right as BigIntLiteralType).value) === str ? right :
left.flags & TypeFlags.Boolean ? left : right.flags & TypeFlags.Boolean ? str === "true" ? trueType : str === "false" ? falseType : booleanType :
left.flags & TypeFlags.BooleanLiteral ? left : right.flags & TypeFlags.BooleanLiteral && (right as IntrinsicType).intrinsicName === str ? right :
left.flags & TypeFlags.Undefined ? left : right.flags & TypeFlags.Undefined && (right as IntrinsicType).intrinsicName === str ? right :
left.flags & TypeFlags.Null ? left : right.flags & TypeFlags.Null && (right as IntrinsicType).intrinsicName === str ? right :
left,
neverType as Type);

if (!(matchingType.flags & TypeFlags.Never)) {
inferFromTypes(matchingType, target);
continue;
}
}
}
}

inferFromTypes(source, target);
}
}
}
Expand Down
2 changes: 1 addition & 1 deletion src/compiler/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5870,7 +5870,7 @@ namespace ts {
AlwaysStrict = 1 << 10, // Always use strict rules for contravariant inferences
MaxValue = 1 << 11, // Seed for inference priority tracking

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

Expand Down
Loading