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

Support relations and inference between template literal types #43361

Merged
merged 5 commits into from
Mar 28, 2021
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
139 changes: 89 additions & 50 deletions src/compiler/checker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14225,9 +14225,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 @@ -14507,7 +14505,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 @@ -18205,13 +18203,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 @@ -20618,43 +20613,89 @@ 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]);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would it be useful to have a singleton emptyTemplateType?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Well, the single-element types array varies here, so we can't use a singleton. We could possibly have a singleton for the ["", ""] array, but it's hardly worth the effort as this code doesn't run that often.

}

function inferFromLiteralPartsToTemplateLiteral(sourceTexts: readonly string[], sourceTypes: readonly Type[], target: TemplateLiteralType): Type[] | undefined {
Copy link
Member

@DanielRosenwasser DanielRosenwasser Mar 26, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't like calling these sourceTexts because it immediately signals to me "BUG" even though you're not actually grabbing out the source text. You're grabbing the normalized text content which is the correct thing to do.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm, I don't think the current names are that bad. After all the properties are called texts and types in template literals and we want the parameters to reflect the relation.

Copy link
Member

@DanielRosenwasser DanielRosenwasser Mar 28, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Right, I think that even texts would be a better name - I may send a follow-up PR but I'm going to merge this for now.

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++) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Some sort of comment (an overview of the algorithm / what it's trying to do, or the name of it if this is a well-known one?) seems justified here

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, I think if you could just explain with examples too, that'd be helpful.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ok, I'll add some comments.

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 @@ -21119,9 +21160,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