Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

More specific inference for constrained 'infer' types in template literal types #48094

Merged
merged 10 commits into from
May 27, 2022
126 changes: 120 additions & 6 deletions src/compiler/checker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -263,6 +263,20 @@ namespace ts {
VoidIsNonOptional = 1 << 1,
}

const enum TemplateTypePlaceholderPriority {
Never, // lowest
KeywordLiterals, // true | false | null | undefined
Boolean,
BigIntLiterals,
BigInt,
NumberLiterals,
Enums,
Number,
StringLiterals,
TemplateLiterals,
String, // highest
}

const enum IntrinsicTypeKind {
Uppercase,
Lowercase,
Expand Down Expand Up @@ -22109,13 +22123,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 +22165,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 +22175,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 @@ -22701,6 +22743,23 @@ namespace ts {
}
}

function getTemplateTypePlaceholderPriority(type: Type) {
return type.flags & TypeFlags.String ? TemplateTypePlaceholderPriority.String :
type.flags & TypeFlags.TemplateLiteral ? TemplateTypePlaceholderPriority.TemplateLiterals :
type.flags & TypeFlags.StringMapping ? TemplateTypePlaceholderPriority.StringLiterals :
type.flags & TypeFlags.StringLiteral ? TemplateTypePlaceholderPriority.StringLiterals :
type.flags & TypeFlags.Number ? TemplateTypePlaceholderPriority.Number :
type.flags & TypeFlags.Enum ? TemplateTypePlaceholderPriority.Enums :
type.flags & TypeFlags.NumberLiteral ? TemplateTypePlaceholderPriority.NumberLiterals :
type.flags & TypeFlags.BigInt ? TemplateTypePlaceholderPriority.BigInt :
type.flags & TypeFlags.BigIntLiteral ? TemplateTypePlaceholderPriority.BigIntLiterals :
type.flags & TypeFlags.Boolean ? TemplateTypePlaceholderPriority.Boolean :
type.flags & TypeFlags.BooleanLiteral ? TemplateTypePlaceholderPriority.KeywordLiterals :
type.flags & TypeFlags.Undefined ? TemplateTypePlaceholderPriority.KeywordLiterals :
type.flags & TypeFlags.Null ? TemplateTypePlaceholderPriority.KeywordLiterals :
TemplateTypePlaceholderPriority.Never;
}

function inferToTemplateLiteralType(source: Type, target: TemplateLiteralType) {
const matches = inferTypesFromTemplateLiteralType(source, target);
const types = target.types;
Expand All @@ -22712,7 +22771,56 @@ 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)) {
let allTypeFlags = reduceType(constraint, (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 = reduceType(constraint, (matchingType, t) =>
!(t.flags & allTypeFlags) || getTemplateTypePlaceholderPriority(t) <= getTemplateTypePlaceholderPriority(matchingType) ? matchingType :
t.flags & TypeFlags.String ? source :
t.flags & TypeFlags.TemplateLiteral && isTypeMatchedByTemplateLiteralType(source, t as TemplateLiteralType) ? source :
t.flags & TypeFlags.StringMapping && str === applyStringMapping(t.symbol, str) ? source :
t.flags & (TypeFlags.Number | TypeFlags.Enum) ? getNumberLiteralType(+str) : // if `str` was not a valid number, TypeFlags.Number and TypeFlags.Enum would have been excluded above.
t.flags & TypeFlags.BigInt ? parseBigIntLiteralType(str) : // if `str` was not a valid bigint, TypeFlags.BigInt would have been excluded above.
t.flags & TypeFlags.Boolean ? str === "true" ? trueType : falseType :
t.flags & TypeFlags.StringLiteral && (t as StringLiteralType).value === str ? t :
t.flags & TypeFlags.NumberLiteral && (t as NumberLiteralType).value === +str ? t :
t.flags & TypeFlags.BigIntLiteral && pseudoBigIntToString((t as BigIntLiteralType).value) === str ? t :
t.flags & (TypeFlags.BooleanLiteral | TypeFlags.Nullable) && (t as IntrinsicType).intrinsicName === str ? t :
matchingType,
neverType as Type);

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

inferFromTypes(source, target);
}
}
}
Expand Down Expand Up @@ -23720,6 +23828,12 @@ namespace ts {
return type.flags & TypeFlags.Union ? forEach((type as UnionType).types, f) : f(type);
}

function reduceType<T>(type: Type, f: (memo: T, t: Type) => T | undefined, initial: T): T;
function reduceType<T>(type: Type, f: (memo: T | undefined, t: Type) => T | undefined): T | undefined;
function reduceType<T>(type: Type, f: (memo: T | undefined, t: Type) => T | undefined, initial?: T | undefined): T | undefined {
return type.flags & TypeFlags.Union ? reduceLeft((type as UnionType).types, f, initial) : f(initial, type);
}

function someType(type: Type, f: (t: Type) => boolean): boolean {
return type.flags & TypeFlags.Union ? some((type as UnionType).types, f) : f(type);
}
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