-
Notifications
You must be signed in to change notification settings - Fork 12.8k
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
Changes from all commits
0e58dcf
fd2284b
2b6b910
510ee0c
f098498
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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; | ||
} | ||
|
||
|
@@ -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) { | ||
|
@@ -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; | ||
} | ||
} | ||
|
||
|
@@ -20618,43 +20613,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 | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I don't like calling these There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Right, I think that even |
||
// 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 = ['<', '.', '>'] | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 There was a problem hiding this comment. Choose a reason for hiding this commentThe 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. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Ok, I'll add some comments. |
||
// 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) { | ||
|
@@ -21119,9 +21179,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]); | ||
|
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; | ||
|
There was a problem hiding this comment.
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
?There was a problem hiding this comment.
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.