Skip to content

Commit

Permalink
Support relations and inference between template literal types (#43361)
Browse files Browse the repository at this point in the history
* Support assignment and inference between template literal types

* Add tests

* Accept new baselines

* Add comments
  • Loading branch information
ahejlsberg authored Mar 28, 2021
1 parent 451089e commit 4774666
Show file tree
Hide file tree
Showing 6 changed files with 1,254 additions and 50 deletions.
158 changes: 108 additions & 50 deletions src/compiler/checker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}

Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -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;
}
}

Expand Down Expand Up @@ -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));
}
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);
}
}

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;
}
matches.push(getLiteralType(str.slice(pos)));
function isValidTypeForTemplateLiteralPlaceholder(source: Type, target: Type): boolean {
if (source === target || target.flags & (TypeFlags.Any | TypeFlags.String)) {
return true;
}
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 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;
}
}
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) {
Expand Down Expand Up @@ -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]);
Expand Down
129 changes: 129 additions & 0 deletions tests/baselines/reference/templateLiteralTypes3.errors.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
tests/cases/conformance/types/literal/templateLiteralTypes3.ts(20,19): error TS2345: Argument of type 'string' is not assignable to parameter of type 'never'.
tests/cases/conformance/types/literal/templateLiteralTypes3.ts(57,5): error TS2322: Type '"hello"' is not assignable to type '`*${string}*`'.
tests/cases/conformance/types/literal/templateLiteralTypes3.ts(69,5): error TS2322: Type '"123"' is not assignable to type '`*${number}*`'.
tests/cases/conformance/types/literal/templateLiteralTypes3.ts(71,5): error TS2322: Type '"**123**"' is not assignable to type '`*${number}*`'.
tests/cases/conformance/types/literal/templateLiteralTypes3.ts(72,5): error TS2322: Type '`*${string}*`' is not assignable to type '`*${number}*`'.
tests/cases/conformance/types/literal/templateLiteralTypes3.ts(74,5): error TS2322: Type '"*false*" | "*true*"' is not assignable to type '`*${number}*`'.
Type '"*false*"' is not assignable to type '`*${number}*`'.


==== tests/cases/conformance/types/literal/templateLiteralTypes3.ts (6 errors) ====
// Inference from template literal type to template literal type

type Foo1<T> = T extends `*${infer U}*` ? U : never;

type T01 = Foo1<'hello'>;
type T02 = Foo1<'*hello*'>;
type T03 = Foo1<'**hello**'>;
type T04 = Foo1<`*${string}*`>;
type T05 = Foo1<`*${number}*`>;
type T06 = Foo1<`*${bigint}*`>;
type T07 = Foo1<`*${any}*`>;
type T08 = Foo1<`**${string}**`>;
type T09 = Foo1<`**${string}**${string}**`>;
type T10 = Foo1<`**${'a' | 'b' | 'c'}**`>;
type T11 = Foo1<`**${boolean}**${boolean}**`>;

declare function foo1<V extends string>(arg: `*${V}*`): V;

function f1<T extends string>(s: string, n: number, b: boolean, t: T) {
let x1 = foo1('hello'); // Error
~~~~~~~
!!! error TS2345: Argument of type 'string' is not assignable to parameter of type 'never'.
let x2 = foo1('*hello*');
let x3 = foo1('**hello**');
let x4 = foo1(`*${s}*` as const);
let x5 = foo1(`*${n}*` as const);
let x6 = foo1(`*${b}*` as const);
let x7 = foo1(`*${t}*` as const);
let x8 = foo1(`**${s}**` as const);
}

// Inference to a placeholder immediately followed by another placeholder infers a single
// character or placeholder from the source.

type Parts<T> =
T extends '' ? [] :
T extends `${infer Head}${infer Tail}` ? [Head, ...Parts<Tail>] :
never;

type T20 = Parts<`abc`>;
type T21 = Parts<`*${string}*`>;
type T22 = Parts<`*${number}*`>;
type T23 = Parts<`*${number}*${string}*${bigint}*`>;

function f2() {
let x: `${number}.${number}.${number}`;
x = '1.1.1';
x = '1.1.1' as `1.1.${number}`;
x = '1.1.1' as `1.${number}.1`;
x = '1.1.1' as `1.${number}.${number}`;
x = '1.1.1' as `${number}.1.1`;
x = '1.1.1' as `${number}.1.${number}`;
x = '1.1.1' as `${number}.${number}.1`;
x = '1.1.1' as `${number}.${number}.${number}`;
}

function f3<T extends string>(s: string, n: number, b: boolean, t: T) {
let x: `*${string}*`;
x = 'hello'; // Error
~
!!! error TS2322: Type '"hello"' is not assignable to type '`*${string}*`'.
x = '*hello*';
x = '**hello**';
x = `*${s}*` as const;
x = `*${n}*` as const;
x = `*${b}*` as const;
x = `*${t}*` as const;
x = `**${s}**` as const;
}

function f4<T extends number>(s: string, n: number, b: boolean, t: T) {
let x: `*${number}*`;
x = '123'; // Error
~
!!! error TS2322: Type '"123"' is not assignable to type '`*${number}*`'.
x = '*123*';
x = '**123**'; // Error
~
!!! error TS2322: Type '"**123**"' is not assignable to type '`*${number}*`'.
x = `*${s}*` as const; // Error
~
!!! error TS2322: Type '`*${string}*`' is not assignable to type '`*${number}*`'.
x = `*${n}*` as const;
x = `*${b}*` as const; // Error
~
!!! error TS2322: Type '"*false*" | "*true*"' is not assignable to type '`*${number}*`'.
!!! error TS2322: Type '"*false*"' is not assignable to type '`*${number}*`'.
x = `*${t}*` as const;
}

// Repro from #43060

type A<T> = T extends `${infer U}.${infer V}` ? U | V : never
type B = A<`test.1024`>; // "test" | "1024"
type C = A<`test.${number}`>; // "test" | `${number}`

type D<T> = T extends `${infer U}.${number}` ? U : never
type E = D<`test.1024`>; // "test"
type F = D<`test.${number}`>; // "test"

type G<T> = T extends `${infer U}.${infer V}` ? U | V : never
type H = G<`test.hoge`>; // "test" | "hoge"
type I = G<`test.${string}`>; // string ("test" | string reduces to string)

type J<T> = T extends `${infer U}.${string}` ? U : never
type K = J<`test.hoge`>; // "test"
type L = J<`test.${string}`>; // "test""

// Repro from #43243

type Templated = `${string} ${string}`;

const value1: string = "abc";
const templated1: Templated = `${value1} abc` as const;
// Type '`${string} abc`' is not assignable to type '`${string} ${string}`'.

const value2 = "abc";
const templated2: Templated = `${value2} abc` as const;

Loading

0 comments on commit 4774666

Please sign in to comment.