Skip to content

Commit 4774666

Browse files
authored
Support relations and inference between template literal types (#43361)
* Support assignment and inference between template literal types * Add tests * Accept new baselines * Add comments
1 parent 451089e commit 4774666

File tree

6 files changed

+1254
-50
lines changed

6 files changed

+1254
-50
lines changed

src/compiler/checker.ts

+108-50
Original file line numberDiff line numberDiff line change
@@ -14295,9 +14295,7 @@ namespace ts {
1429514295
return type.flags & TypeFlags.StringLiteral ? (<StringLiteralType>type).value :
1429614296
type.flags & TypeFlags.NumberLiteral ? "" + (<NumberLiteralType>type).value :
1429714297
type.flags & TypeFlags.BigIntLiteral ? pseudoBigIntToString((<BigIntLiteralType>type).value) :
14298-
type.flags & TypeFlags.BooleanLiteral ? (<IntrinsicType>type).intrinsicName :
14299-
type.flags & TypeFlags.Null ? "null" :
14300-
type.flags & TypeFlags.Undefined ? "undefined" :
14298+
type.flags & (TypeFlags.BooleanLiteral | TypeFlags.Nullable) ? (<IntrinsicType>type).intrinsicName :
1430114299
undefined;
1430214300
}
1430314301

@@ -14577,7 +14575,7 @@ namespace ts {
1457714575
}
1457814576

1457914577
function isPatternLiteralPlaceholderType(type: Type) {
14580-
return templateConstraintType.types.indexOf(type) !== -1 || !!(type.flags & TypeFlags.Any);
14578+
return !!(type.flags & (TypeFlags.Any | TypeFlags.String | TypeFlags.Number | TypeFlags.BigInt));
1458114579
}
1458214580

1458314581
function isPatternLiteralType(type: Type) {
@@ -18275,13 +18273,10 @@ namespace ts {
1827518273
return localResult;
1827618274
}
1827718275
}
18278-
else if (target.flags & TypeFlags.TemplateLiteral && source.flags & TypeFlags.StringLiteral) {
18279-
if (isPatternLiteralType(target)) {
18280-
// match all non-`string` segments
18281-
const result = inferLiteralsFromTemplateLiteralType(source as StringLiteralType, target as TemplateLiteralType);
18282-
if (result && every(result, (r, i) => isStringLiteralTypeValueParsableAsType(r, (target as TemplateLiteralType).types[i]))) {
18283-
return Ternary.True;
18284-
}
18276+
else if (target.flags & TypeFlags.TemplateLiteral) {
18277+
const result = inferTypesFromTemplateLiteralType(source, target as TemplateLiteralType);
18278+
if (result && every(result, (r, i) => isValidTypeForTemplateLiteralPlaceholder(r, (target as TemplateLiteralType).types[i]))) {
18279+
return Ternary.True;
1828518280
}
1828618281
}
1828718282

@@ -20688,43 +20683,108 @@ namespace ts {
2068820683
return success && result === SyntaxKind.BigIntLiteral && scanner.getTextPos() === (s.length + 1) && !(flags & TokenFlags.ContainsSeparator);
2068920684
}
2069020685

20691-
function isStringLiteralTypeValueParsableAsType(s: StringLiteralType, target: Type): boolean {
20692-
if (target.flags & TypeFlags.Union) {
20693-
return someType(target, t => isStringLiteralTypeValueParsableAsType(s, t));
20694-
}
20695-
switch (target) {
20696-
case stringType: return true;
20697-
case numberType: return s.value !== "" && isFinite(+(s.value));
20698-
case bigintType: return s.value !== "" && isValidBigIntString(s.value);
20699-
// the next 4 should be handled in `getTemplateLiteralType`, as they are all exactly one value, but are here for completeness, just in case
20700-
// this function is ever used on types which don't come from template literal holes
20701-
case trueType: return s.value === "true";
20702-
case falseType: return s.value === "false";
20703-
case undefinedType: return s.value === "undefined";
20704-
case nullType: return s.value === "null";
20705-
default: return !!(target.flags & TypeFlags.Any);
20706-
}
20707-
}
20708-
20709-
function inferLiteralsFromTemplateLiteralType(source: StringLiteralType, target: TemplateLiteralType): StringLiteralType[] | undefined {
20710-
const value = source.value;
20711-
const texts = target.texts;
20712-
const lastIndex = texts.length - 1;
20713-
const startText = texts[0];
20714-
const endText = texts[lastIndex];
20715-
if (!(value.startsWith(startText) && value.slice(startText.length).endsWith(endText))) return undefined;
20716-
const matches = [];
20717-
const str = value.slice(startText.length, value.length - endText.length);
20718-
let pos = 0;
20719-
for (let i = 1; i < lastIndex; i++) {
20720-
const delim = texts[i];
20721-
const delimPos = delim.length > 0 ? str.indexOf(delim, pos) : pos < str.length ? pos + 1 : -1;
20722-
if (delimPos < 0) return undefined;
20723-
matches.push(getLiteralType(str.slice(pos, delimPos)));
20724-
pos = delimPos + delim.length;
20725-
}
20726-
matches.push(getLiteralType(str.slice(pos)));
20686+
function isValidTypeForTemplateLiteralPlaceholder(source: Type, target: Type): boolean {
20687+
if (source === target || target.flags & (TypeFlags.Any | TypeFlags.String)) {
20688+
return true;
20689+
}
20690+
if (source.flags & TypeFlags.StringLiteral) {
20691+
const value = (<StringLiteralType>source).value;
20692+
return !!(target.flags & TypeFlags.Number && value !== "" && isFinite(+value) ||
20693+
target.flags & TypeFlags.BigInt && value !== "" && isValidBigIntString(value) ||
20694+
target.flags & (TypeFlags.BooleanLiteral | TypeFlags.Nullable) && value === (<IntrinsicType>target).intrinsicName);
20695+
}
20696+
if (source.flags & TypeFlags.TemplateLiteral) {
20697+
const texts = (<TemplateLiteralType>source).texts;
20698+
return texts.length === 2 && texts[0] === "" && texts[1] === "" && isTypeAssignableTo((<TemplateLiteralType>source).types[0], target);
20699+
}
20700+
return isTypeAssignableTo(source, target);
20701+
}
20702+
20703+
function inferTypesFromTemplateLiteralType(source: Type, target: TemplateLiteralType): Type[] | undefined {
20704+
return source.flags & TypeFlags.StringLiteral ? inferFromLiteralPartsToTemplateLiteral([(<StringLiteralType>source).value], emptyArray, target) :
20705+
source.flags & TypeFlags.TemplateLiteral ?
20706+
arraysEqual((<TemplateLiteralType>source).texts, target.texts) ? map((<TemplateLiteralType>source).types, getStringLikeTypeForType) :
20707+
inferFromLiteralPartsToTemplateLiteral((<TemplateLiteralType>source).texts, (<TemplateLiteralType>source).types, target) :
20708+
undefined;
20709+
}
20710+
20711+
function getStringLikeTypeForType(type: Type) {
20712+
return type.flags & (TypeFlags.Any | TypeFlags.StringLike) ? type : getTemplateLiteralType(["", ""], [type]);
20713+
}
20714+
20715+
// This function infers from the text parts and type parts of a source literal to a target template literal. The number
20716+
// of text parts is always one more than the number of type parts, and a source string literal is treated as a source
20717+
// with one text part and zero type parts. The function returns an array of inferred string or template literal types
20718+
// corresponding to the placeholders in the target template literal, or undefined if the source doesn't match the target.
20719+
//
20720+
// We first check that the starting source text part matches the starting target text part, and that the ending source
20721+
// text part ends matches the ending target text part. We then iterate through the remaining target text parts, finding
20722+
// a match for each in the source and inferring string or template literal types created from the segments of the source
20723+
// that occur between the matches. During this iteration, seg holds the index of the current text part in the sourceTexts
20724+
// array and pos holds the current character position in the current text part.
20725+
//
20726+
// Consider inference from type `<<${string}>.<${number}-${number}>>` to type `<${string}.${string}>`, i.e.
20727+
// sourceTexts = ['<<', '>.<', '-', '>>']
20728+
// sourceTypes = [string, number, number]
20729+
// target.texts = ['<', '.', '>']
20730+
// We first match '<' in the target to the start of '<<' in the source and '>' in the target to the end of '>>' in
20731+
// the source. The first match for the '.' in target occurs at character 1 in the source text part at index 1, and thus
20732+
// the first inference is the template literal type `<${string}>`. The remainder of the source makes up the second
20733+
// inference, the template literal type `<${number}-${number}>`.
20734+
function inferFromLiteralPartsToTemplateLiteral(sourceTexts: readonly string[], sourceTypes: readonly Type[], target: TemplateLiteralType): Type[] | undefined {
20735+
const lastSourceIndex = sourceTexts.length - 1;
20736+
const sourceStartText = sourceTexts[0];
20737+
const sourceEndText = sourceTexts[lastSourceIndex];
20738+
const targetTexts = target.texts;
20739+
const lastTargetIndex = targetTexts.length - 1;
20740+
const targetStartText = targetTexts[0];
20741+
const targetEndText = targetTexts[lastTargetIndex];
20742+
if (lastSourceIndex === 0 && sourceStartText.length < targetStartText.length + targetEndText.length ||
20743+
!sourceStartText.startsWith(targetStartText) || !sourceEndText.endsWith(targetEndText)) return undefined;
20744+
const remainingEndText = sourceEndText.slice(0, sourceEndText.length - targetEndText.length);
20745+
const matches: Type[] = [];
20746+
let seg = 0;
20747+
let pos = targetStartText.length;
20748+
for (let i = 1; i < lastTargetIndex; i++) {
20749+
const delim = targetTexts[i];
20750+
if (delim.length > 0) {
20751+
let s = seg;
20752+
let p = pos;
20753+
while (true) {
20754+
p = getSourceText(s).indexOf(delim, p);
20755+
if (p >= 0) break;
20756+
s++;
20757+
if (s === sourceTexts.length) return undefined;
20758+
p = 0;
20759+
}
20760+
addMatch(s, p);
20761+
pos += delim.length;
20762+
}
20763+
else if (pos < getSourceText(seg).length) {
20764+
addMatch(seg, pos + 1);
20765+
}
20766+
else if (seg < lastSourceIndex) {
20767+
addMatch(seg + 1, 0);
20768+
}
20769+
else {
20770+
return undefined;
20771+
}
20772+
}
20773+
addMatch(lastSourceIndex, getSourceText(lastSourceIndex).length);
2072720774
return matches;
20775+
function getSourceText(index: number) {
20776+
return index < lastSourceIndex ? sourceTexts[index] : remainingEndText;
20777+
}
20778+
function addMatch(s: number, p: number) {
20779+
const matchType = s === seg ?
20780+
getLiteralType(getSourceText(s).slice(pos, p)) :
20781+
getTemplateLiteralType(
20782+
[sourceTexts[seg].slice(pos), ...sourceTexts.slice(seg + 1, s), getSourceText(s).slice(0, p)],
20783+
sourceTypes.slice(seg, s));
20784+
matches.push(matchType);
20785+
seg = s;
20786+
pos = p;
20787+
}
2072820788
}
2072920789

2073020790
function inferTypes(inferences: InferenceInfo[], originalSource: Type, originalTarget: Type, priority: InferencePriority = 0, contravariant = false) {
@@ -21189,9 +21249,7 @@ namespace ts {
2118921249
}
2119021250

2119121251
function inferToTemplateLiteralType(source: Type, target: TemplateLiteralType) {
21192-
const matches = source.flags & TypeFlags.StringLiteral ? inferLiteralsFromTemplateLiteralType(<StringLiteralType>source, target) :
21193-
source.flags & TypeFlags.TemplateLiteral && arraysEqual((<TemplateLiteralType>source).texts, target.texts) ? (<TemplateLiteralType>source).types :
21194-
undefined;
21252+
const matches = inferTypesFromTemplateLiteralType(source, target);
2119521253
const types = target.types;
2119621254
for (let i = 0; i < types.length; i++) {
2119721255
inferFromTypes(matches ? matches[i] : neverType, types[i]);
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,129 @@
1+
tests/cases/conformance/types/literal/templateLiteralTypes3.ts(20,19): error TS2345: Argument of type 'string' is not assignable to parameter of type 'never'.
2+
tests/cases/conformance/types/literal/templateLiteralTypes3.ts(57,5): error TS2322: Type '"hello"' is not assignable to type '`*${string}*`'.
3+
tests/cases/conformance/types/literal/templateLiteralTypes3.ts(69,5): error TS2322: Type '"123"' is not assignable to type '`*${number}*`'.
4+
tests/cases/conformance/types/literal/templateLiteralTypes3.ts(71,5): error TS2322: Type '"**123**"' is not assignable to type '`*${number}*`'.
5+
tests/cases/conformance/types/literal/templateLiteralTypes3.ts(72,5): error TS2322: Type '`*${string}*`' is not assignable to type '`*${number}*`'.
6+
tests/cases/conformance/types/literal/templateLiteralTypes3.ts(74,5): error TS2322: Type '"*false*" | "*true*"' is not assignable to type '`*${number}*`'.
7+
Type '"*false*"' is not assignable to type '`*${number}*`'.
8+
9+
10+
==== tests/cases/conformance/types/literal/templateLiteralTypes3.ts (6 errors) ====
11+
// Inference from template literal type to template literal type
12+
13+
type Foo1<T> = T extends `*${infer U}*` ? U : never;
14+
15+
type T01 = Foo1<'hello'>;
16+
type T02 = Foo1<'*hello*'>;
17+
type T03 = Foo1<'**hello**'>;
18+
type T04 = Foo1<`*${string}*`>;
19+
type T05 = Foo1<`*${number}*`>;
20+
type T06 = Foo1<`*${bigint}*`>;
21+
type T07 = Foo1<`*${any}*`>;
22+
type T08 = Foo1<`**${string}**`>;
23+
type T09 = Foo1<`**${string}**${string}**`>;
24+
type T10 = Foo1<`**${'a' | 'b' | 'c'}**`>;
25+
type T11 = Foo1<`**${boolean}**${boolean}**`>;
26+
27+
declare function foo1<V extends string>(arg: `*${V}*`): V;
28+
29+
function f1<T extends string>(s: string, n: number, b: boolean, t: T) {
30+
let x1 = foo1('hello'); // Error
31+
~~~~~~~
32+
!!! error TS2345: Argument of type 'string' is not assignable to parameter of type 'never'.
33+
let x2 = foo1('*hello*');
34+
let x3 = foo1('**hello**');
35+
let x4 = foo1(`*${s}*` as const);
36+
let x5 = foo1(`*${n}*` as const);
37+
let x6 = foo1(`*${b}*` as const);
38+
let x7 = foo1(`*${t}*` as const);
39+
let x8 = foo1(`**${s}**` as const);
40+
}
41+
42+
// Inference to a placeholder immediately followed by another placeholder infers a single
43+
// character or placeholder from the source.
44+
45+
type Parts<T> =
46+
T extends '' ? [] :
47+
T extends `${infer Head}${infer Tail}` ? [Head, ...Parts<Tail>] :
48+
never;
49+
50+
type T20 = Parts<`abc`>;
51+
type T21 = Parts<`*${string}*`>;
52+
type T22 = Parts<`*${number}*`>;
53+
type T23 = Parts<`*${number}*${string}*${bigint}*`>;
54+
55+
function f2() {
56+
let x: `${number}.${number}.${number}`;
57+
x = '1.1.1';
58+
x = '1.1.1' as `1.1.${number}`;
59+
x = '1.1.1' as `1.${number}.1`;
60+
x = '1.1.1' as `1.${number}.${number}`;
61+
x = '1.1.1' as `${number}.1.1`;
62+
x = '1.1.1' as `${number}.1.${number}`;
63+
x = '1.1.1' as `${number}.${number}.1`;
64+
x = '1.1.1' as `${number}.${number}.${number}`;
65+
}
66+
67+
function f3<T extends string>(s: string, n: number, b: boolean, t: T) {
68+
let x: `*${string}*`;
69+
x = 'hello'; // Error
70+
~
71+
!!! error TS2322: Type '"hello"' is not assignable to type '`*${string}*`'.
72+
x = '*hello*';
73+
x = '**hello**';
74+
x = `*${s}*` as const;
75+
x = `*${n}*` as const;
76+
x = `*${b}*` as const;
77+
x = `*${t}*` as const;
78+
x = `**${s}**` as const;
79+
}
80+
81+
function f4<T extends number>(s: string, n: number, b: boolean, t: T) {
82+
let x: `*${number}*`;
83+
x = '123'; // Error
84+
~
85+
!!! error TS2322: Type '"123"' is not assignable to type '`*${number}*`'.
86+
x = '*123*';
87+
x = '**123**'; // Error
88+
~
89+
!!! error TS2322: Type '"**123**"' is not assignable to type '`*${number}*`'.
90+
x = `*${s}*` as const; // Error
91+
~
92+
!!! error TS2322: Type '`*${string}*`' is not assignable to type '`*${number}*`'.
93+
x = `*${n}*` as const;
94+
x = `*${b}*` as const; // Error
95+
~
96+
!!! error TS2322: Type '"*false*" | "*true*"' is not assignable to type '`*${number}*`'.
97+
!!! error TS2322: Type '"*false*"' is not assignable to type '`*${number}*`'.
98+
x = `*${t}*` as const;
99+
}
100+
101+
// Repro from #43060
102+
103+
type A<T> = T extends `${infer U}.${infer V}` ? U | V : never
104+
type B = A<`test.1024`>; // "test" | "1024"
105+
type C = A<`test.${number}`>; // "test" | `${number}`
106+
107+
type D<T> = T extends `${infer U}.${number}` ? U : never
108+
type E = D<`test.1024`>; // "test"
109+
type F = D<`test.${number}`>; // "test"
110+
111+
type G<T> = T extends `${infer U}.${infer V}` ? U | V : never
112+
type H = G<`test.hoge`>; // "test" | "hoge"
113+
type I = G<`test.${string}`>; // string ("test" | string reduces to string)
114+
115+
type J<T> = T extends `${infer U}.${string}` ? U : never
116+
type K = J<`test.hoge`>; // "test"
117+
type L = J<`test.${string}`>; // "test""
118+
119+
// Repro from #43243
120+
121+
type Templated = `${string} ${string}`;
122+
123+
const value1: string = "abc";
124+
const templated1: Templated = `${value1} abc` as const;
125+
// Type '`${string} abc`' is not assignable to type '`${string} ${string}`'.
126+
127+
const value2 = "abc";
128+
const templated2: Templated = `${value2} abc` as const;
129+

0 commit comments

Comments
 (0)