-
Notifications
You must be signed in to change notification settings - Fork 12.6k
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 4 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,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]); | ||
} | ||
|
||
function inferFromLiteralPartsToTemplateLiteral(sourceTexts: readonly string[], sourceTypes: readonly Type[], target: TemplateLiteralType): Type[] | undefined { | ||
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 |
||
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++) { | ||
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. |
||
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 +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]); | ||
|
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.