mirror of
https://github.com/microsoft/TypeScript.git
synced 2026-06-17 21:09:09 -05:00
Support relations and inference between template literal types (#43361)
* Support assignment and inference between template literal types * Add tests * Accept new baselines * Add comments
This commit is contained in:
@@ -14295,9 +14295,7 @@ namespace ts {
|
||||
return type.flags & TypeFlags.StringLiteral ? (<StringLiteralType>type).value :
|
||||
type.flags & TypeFlags.NumberLiteral ? "" + (<NumberLiteralType>type).value :
|
||||
type.flags & TypeFlags.BigIntLiteral ? pseudoBigIntToString((<BigIntLiteralType>type).value) :
|
||||
type.flags & TypeFlags.BooleanLiteral ? (<IntrinsicType>type).intrinsicName :
|
||||
type.flags & TypeFlags.Null ? "null" :
|
||||
type.flags & TypeFlags.Undefined ? "undefined" :
|
||||
type.flags & (TypeFlags.BooleanLiteral | TypeFlags.Nullable) ? (<IntrinsicType>type).intrinsicName :
|
||||
undefined;
|
||||
}
|
||||
|
||||
@@ -14577,7 +14575,7 @@ namespace ts {
|
||||
}
|
||||
|
||||
function isPatternLiteralPlaceholderType(type: Type) {
|
||||
return templateConstraintType.types.indexOf(type) !== -1 || !!(type.flags & TypeFlags.Any);
|
||||
return !!(type.flags & (TypeFlags.Any | TypeFlags.String | TypeFlags.Number | TypeFlags.BigInt));
|
||||
}
|
||||
|
||||
function isPatternLiteralType(type: Type) {
|
||||
@@ -18275,13 +18273,10 @@ namespace ts {
|
||||
return localResult;
|
||||
}
|
||||
}
|
||||
else if (target.flags & TypeFlags.TemplateLiteral && source.flags & TypeFlags.StringLiteral) {
|
||||
if (isPatternLiteralType(target)) {
|
||||
// match all non-`string` segments
|
||||
const result = inferLiteralsFromTemplateLiteralType(source as StringLiteralType, target as TemplateLiteralType);
|
||||
if (result && every(result, (r, i) => isStringLiteralTypeValueParsableAsType(r, (target as TemplateLiteralType).types[i]))) {
|
||||
return Ternary.True;
|
||||
}
|
||||
else if (target.flags & TypeFlags.TemplateLiteral) {
|
||||
const result = inferTypesFromTemplateLiteralType(source, target as TemplateLiteralType);
|
||||
if (result && every(result, (r, i) => isValidTypeForTemplateLiteralPlaceholder(r, (target as TemplateLiteralType).types[i]))) {
|
||||
return Ternary.True;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -20688,43 +20683,108 @@ namespace ts {
|
||||
return success && result === SyntaxKind.BigIntLiteral && scanner.getTextPos() === (s.length + 1) && !(flags & TokenFlags.ContainsSeparator);
|
||||
}
|
||||
|
||||
function isStringLiteralTypeValueParsableAsType(s: StringLiteralType, target: Type): boolean {
|
||||
if (target.flags & TypeFlags.Union) {
|
||||
return someType(target, t => isStringLiteralTypeValueParsableAsType(s, t));
|
||||
function isValidTypeForTemplateLiteralPlaceholder(source: Type, target: Type): boolean {
|
||||
if (source === target || target.flags & (TypeFlags.Any | TypeFlags.String)) {
|
||||
return true;
|
||||
}
|
||||
switch (target) {
|
||||
case stringType: return true;
|
||||
case numberType: return s.value !== "" && isFinite(+(s.value));
|
||||
case bigintType: return s.value !== "" && isValidBigIntString(s.value);
|
||||
// the next 4 should be handled in `getTemplateLiteralType`, as they are all exactly one value, but are here for completeness, just in case
|
||||
// this function is ever used on types which don't come from template literal holes
|
||||
case trueType: return s.value === "true";
|
||||
case falseType: return s.value === "false";
|
||||
case undefinedType: return s.value === "undefined";
|
||||
case nullType: return s.value === "null";
|
||||
default: return !!(target.flags & TypeFlags.Any);
|
||||
if (source.flags & TypeFlags.StringLiteral) {
|
||||
const value = (<StringLiteralType>source).value;
|
||||
return !!(target.flags & TypeFlags.Number && value !== "" && isFinite(+value) ||
|
||||
target.flags & TypeFlags.BigInt && value !== "" && isValidBigIntString(value) ||
|
||||
target.flags & (TypeFlags.BooleanLiteral | TypeFlags.Nullable) && value === (<IntrinsicType>target).intrinsicName);
|
||||
}
|
||||
if (source.flags & TypeFlags.TemplateLiteral) {
|
||||
const texts = (<TemplateLiteralType>source).texts;
|
||||
return texts.length === 2 && texts[0] === "" && texts[1] === "" && isTypeAssignableTo((<TemplateLiteralType>source).types[0], target);
|
||||
}
|
||||
return isTypeAssignableTo(source, target);
|
||||
}
|
||||
|
||||
function inferLiteralsFromTemplateLiteralType(source: StringLiteralType, target: TemplateLiteralType): StringLiteralType[] | undefined {
|
||||
const value = source.value;
|
||||
const texts = target.texts;
|
||||
const lastIndex = texts.length - 1;
|
||||
const startText = texts[0];
|
||||
const endText = texts[lastIndex];
|
||||
if (!(value.startsWith(startText) && value.slice(startText.length).endsWith(endText))) return undefined;
|
||||
const matches = [];
|
||||
const str = value.slice(startText.length, value.length - endText.length);
|
||||
let pos = 0;
|
||||
for (let i = 1; i < lastIndex; i++) {
|
||||
const delim = texts[i];
|
||||
const delimPos = delim.length > 0 ? str.indexOf(delim, pos) : pos < str.length ? pos + 1 : -1;
|
||||
if (delimPos < 0) return undefined;
|
||||
matches.push(getLiteralType(str.slice(pos, delimPos)));
|
||||
pos = delimPos + delim.length;
|
||||
function inferTypesFromTemplateLiteralType(source: Type, target: TemplateLiteralType): Type[] | undefined {
|
||||
return source.flags & TypeFlags.StringLiteral ? inferFromLiteralPartsToTemplateLiteral([(<StringLiteralType>source).value], emptyArray, target) :
|
||||
source.flags & TypeFlags.TemplateLiteral ?
|
||||
arraysEqual((<TemplateLiteralType>source).texts, target.texts) ? map((<TemplateLiteralType>source).types, getStringLikeTypeForType) :
|
||||
inferFromLiteralPartsToTemplateLiteral((<TemplateLiteralType>source).texts, (<TemplateLiteralType>source).types, target) :
|
||||
undefined;
|
||||
}
|
||||
|
||||
function getStringLikeTypeForType(type: Type) {
|
||||
return type.flags & (TypeFlags.Any | TypeFlags.StringLike) ? type : getTemplateLiteralType(["", ""], [type]);
|
||||
}
|
||||
|
||||
// This function infers from the text parts and type parts of a source literal to a target template literal. The number
|
||||
// of text parts is always one more than the number of type parts, and a source string literal is treated as a source
|
||||
// with one text part and zero type parts. The function returns an array of inferred string or template literal types
|
||||
// corresponding to the placeholders in the target template literal, or undefined if the source doesn't match the target.
|
||||
//
|
||||
// We first check that the starting source text part matches the starting target text part, and that the ending source
|
||||
// text part ends matches the ending target text part. We then iterate through the remaining target text parts, finding
|
||||
// a match for each in the source and inferring string or template literal types created from the segments of the source
|
||||
// that occur between the matches. During this iteration, seg holds the index of the current text part in the sourceTexts
|
||||
// array and pos holds the current character position in the current text part.
|
||||
//
|
||||
// Consider inference from type `<<${string}>.<${number}-${number}>>` to type `<${string}.${string}>`, i.e.
|
||||
// sourceTexts = ['<<', '>.<', '-', '>>']
|
||||
// sourceTypes = [string, number, number]
|
||||
// target.texts = ['<', '.', '>']
|
||||
// We first match '<' in the target to the start of '<<' in the source and '>' in the target to the end of '>>' in
|
||||
// the source. The first match for the '.' in target occurs at character 1 in the source text part at index 1, and thus
|
||||
// the first inference is the template literal type `<${string}>`. The remainder of the source makes up the second
|
||||
// inference, the template literal type `<${number}-${number}>`.
|
||||
function inferFromLiteralPartsToTemplateLiteral(sourceTexts: readonly string[], sourceTypes: readonly Type[], target: TemplateLiteralType): Type[] | undefined {
|
||||
const lastSourceIndex = sourceTexts.length - 1;
|
||||
const sourceStartText = sourceTexts[0];
|
||||
const sourceEndText = sourceTexts[lastSourceIndex];
|
||||
const targetTexts = target.texts;
|
||||
const lastTargetIndex = targetTexts.length - 1;
|
||||
const targetStartText = targetTexts[0];
|
||||
const targetEndText = targetTexts[lastTargetIndex];
|
||||
if (lastSourceIndex === 0 && sourceStartText.length < targetStartText.length + targetEndText.length ||
|
||||
!sourceStartText.startsWith(targetStartText) || !sourceEndText.endsWith(targetEndText)) return undefined;
|
||||
const remainingEndText = sourceEndText.slice(0, sourceEndText.length - targetEndText.length);
|
||||
const matches: Type[] = [];
|
||||
let seg = 0;
|
||||
let pos = targetStartText.length;
|
||||
for (let i = 1; i < lastTargetIndex; i++) {
|
||||
const delim = targetTexts[i];
|
||||
if (delim.length > 0) {
|
||||
let s = seg;
|
||||
let p = pos;
|
||||
while (true) {
|
||||
p = getSourceText(s).indexOf(delim, p);
|
||||
if (p >= 0) break;
|
||||
s++;
|
||||
if (s === sourceTexts.length) return undefined;
|
||||
p = 0;
|
||||
}
|
||||
addMatch(s, p);
|
||||
pos += delim.length;
|
||||
}
|
||||
else if (pos < getSourceText(seg).length) {
|
||||
addMatch(seg, pos + 1);
|
||||
}
|
||||
else if (seg < lastSourceIndex) {
|
||||
addMatch(seg + 1, 0);
|
||||
}
|
||||
else {
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
matches.push(getLiteralType(str.slice(pos)));
|
||||
addMatch(lastSourceIndex, getSourceText(lastSourceIndex).length);
|
||||
return matches;
|
||||
function getSourceText(index: number) {
|
||||
return index < lastSourceIndex ? sourceTexts[index] : remainingEndText;
|
||||
}
|
||||
function addMatch(s: number, p: number) {
|
||||
const matchType = s === seg ?
|
||||
getLiteralType(getSourceText(s).slice(pos, p)) :
|
||||
getTemplateLiteralType(
|
||||
[sourceTexts[seg].slice(pos), ...sourceTexts.slice(seg + 1, s), getSourceText(s).slice(0, p)],
|
||||
sourceTypes.slice(seg, s));
|
||||
matches.push(matchType);
|
||||
seg = s;
|
||||
pos = p;
|
||||
}
|
||||
}
|
||||
|
||||
function inferTypes(inferences: InferenceInfo[], originalSource: Type, originalTarget: Type, priority: InferencePriority = 0, contravariant = false) {
|
||||
@@ -21189,9 +21249,7 @@ namespace ts {
|
||||
}
|
||||
|
||||
function inferToTemplateLiteralType(source: Type, target: TemplateLiteralType) {
|
||||
const matches = source.flags & TypeFlags.StringLiteral ? inferLiteralsFromTemplateLiteralType(<StringLiteralType>source, target) :
|
||||
source.flags & TypeFlags.TemplateLiteral && arraysEqual((<TemplateLiteralType>source).texts, target.texts) ? (<TemplateLiteralType>source).types :
|
||||
undefined;
|
||||
const matches = inferTypesFromTemplateLiteralType(source, target);
|
||||
const types = target.types;
|
||||
for (let i = 0; i < types.length; i++) {
|
||||
inferFromTypes(matches ? matches[i] : neverType, types[i]);
|
||||
|
||||
Reference in New Issue
Block a user