Skip to content

Commit a960463

Browse files
authored
Allow pattern literal types like http://${string} to exist and be reasoned about (#40598)
* Allow pattern literal types like `http://${string}` to exist and be reasoned about * Allow bigint, number, null, and undefined in template holes * Add test of the trivia case * Handle `any` in template holes, add assignability rules for template -> template relations * Explicitly test concatenated patterns * PR Feedback
1 parent a91c287 commit a960463

10 files changed

+1856
-41
lines changed

src/compiler/checker.ts

Lines changed: 122 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -771,7 +771,7 @@ namespace ts {
771771
const stringNumberSymbolType = getUnionType([stringType, numberType, esSymbolType]);
772772
const keyofConstraintType = keyofStringsOnly ? stringType : stringNumberSymbolType;
773773
const numberOrBigIntType = getUnionType([numberType, bigintType]);
774-
const templateConstraintType = getUnionType([stringType, numberType, booleanType, bigintType]);
774+
const templateConstraintType = getUnionType([stringType, numberType, booleanType, bigintType, nullType, undefinedType]) as UnionType;
775775

776776
const restrictiveMapper: TypeMapper = makeFunctionTypeMapper(t => t.flags & TypeFlags.TypeParameter ? getRestrictiveTypeParameter(<TypeParameter>t) : t);
777777
const permissiveMapper: TypeMapper = makeFunctionTypeMapper(t => t.flags & TypeFlags.TypeParameter ? wildcardType : t);
@@ -13207,6 +13207,30 @@ namespace ts {
1320713207
return true;
1320813208
}
1320913209

13210+
/**
13211+
* Returns `true` if the intersection of the template literals and string literals is the empty set, eg `get${string}` & "setX", and should reduce to `never`
13212+
*/
13213+
function extractRedundantTemplateLiterals(types: Type[]): boolean {
13214+
let i = types.length;
13215+
const literals = filter(types, t => !!(t.flags & TypeFlags.StringLiteral));
13216+
while (i > 0) {
13217+
i--;
13218+
const t = types[i];
13219+
if (!(t.flags & TypeFlags.TemplateLiteral)) continue;
13220+
for (const t2 of literals) {
13221+
if (isTypeSubtypeOf(t2, t)) {
13222+
// eg, ``get${T}` & "getX"` is just `"getX"`
13223+
orderedRemoveItemAt(types, i);
13224+
break;
13225+
}
13226+
else if (isPatternLiteralType(t)) {
13227+
return true;
13228+
}
13229+
}
13230+
}
13231+
return false;
13232+
}
13233+
1321013234
function extractIrreducible(types: Type[], flag: TypeFlags) {
1321113235
if (every(types, t => !!(t.flags & TypeFlags.Union) && some((t as UnionType).types, tt => !!(tt.flags & flag)))) {
1321213236
for (let i = 0; i < types.length; i++) {
@@ -13355,7 +13379,12 @@ namespace ts {
1335513379
}
1335613380
}
1335713381
else {
13358-
result = createIntersectionType(typeSet, aliasSymbol, aliasTypeArguments);
13382+
if (includes & TypeFlags.TemplateLiteral && includes & TypeFlags.StringLiteral && extractRedundantTemplateLiterals(typeSet)) {
13383+
result = neverType;
13384+
}
13385+
else {
13386+
result = createIntersectionType(typeSet, aliasSymbol, aliasTypeArguments);
13387+
}
1335913388
}
1336013389
intersectionTypes.set(id, result);
1336113390
}
@@ -13531,7 +13560,7 @@ namespace ts {
1353113560
function addSpans(texts: readonly string[], types: readonly Type[]): boolean {
1353213561
for (let i = 0; i < types.length; i++) {
1353313562
const t = types[i];
13534-
if (t.flags & TypeFlags.Literal) {
13563+
if (t.flags & (TypeFlags.Literal | TypeFlags.Null | TypeFlags.Undefined)) {
1353513564
text += getTemplateStringForType(t) || "";
1353613565
text += texts[i + 1];
1353713566
}
@@ -13540,7 +13569,7 @@ namespace ts {
1354013569
if (!addSpans((<TemplateLiteralType>t).texts, (<TemplateLiteralType>t).types)) return false;
1354113570
text += texts[i + 1];
1354213571
}
13543-
else if (isGenericIndexType(t)) {
13572+
else if (isGenericIndexType(t) || isPatternLiteralPlaceholderType(t)) {
1354413573
newTypes.push(t);
1354513574
newTexts.push(text);
1354613575
text = texts[i + 1];
@@ -13558,6 +13587,8 @@ namespace ts {
1355813587
type.flags & TypeFlags.NumberLiteral ? "" + (<NumberLiteralType>type).value :
1355913588
type.flags & TypeFlags.BigIntLiteral ? pseudoBigIntToString((<BigIntLiteralType>type).value) :
1356013589
type.flags & TypeFlags.BooleanLiteral ? (<IntrinsicType>type).intrinsicName :
13590+
type.flags & TypeFlags.Null ? "null" :
13591+
type.flags & TypeFlags.Undefined ? "undefined" :
1356113592
undefined;
1356213593
}
1356313594

@@ -13817,6 +13848,14 @@ namespace ts {
1381713848
accessNode;
1381813849
}
1381913850

13851+
function isPatternLiteralPlaceholderType(type: Type) {
13852+
return templateConstraintType.types.indexOf(type) !== -1 || !!(type.flags & TypeFlags.Any);
13853+
}
13854+
13855+
function isPatternLiteralType(type: Type) {
13856+
return !!(type.flags & TypeFlags.TemplateLiteral) && every((type as TemplateLiteralType).types, isPatternLiteralPlaceholderType);
13857+
}
13858+
1382013859
function isGenericObjectType(type: Type): boolean {
1382113860
if (type.flags & TypeFlags.UnionOrIntersection) {
1382213861
if (!((<UnionOrIntersectionType>type).objectFlags & ObjectFlags.IsGenericObjectTypeComputed)) {
@@ -13836,7 +13875,7 @@ namespace ts {
1383613875
}
1383713876
return !!((<UnionOrIntersectionType>type).objectFlags & ObjectFlags.IsGenericIndexType);
1383813877
}
13839-
return !!(type.flags & (TypeFlags.InstantiableNonPrimitive | TypeFlags.Index | TypeFlags.TemplateLiteral | TypeFlags.StringMapping));
13878+
return !!(type.flags & (TypeFlags.InstantiableNonPrimitive | TypeFlags.Index | TypeFlags.TemplateLiteral | TypeFlags.StringMapping)) && !isPatternLiteralType(type);
1384013879
}
1384113880

1384213881
function isThisTypeParameter(type: Type): boolean {
@@ -14562,6 +14601,8 @@ namespace ts {
1456214601
return !!(type.flags & TypeFlags.Literal) && (<LiteralType>type).freshType === type;
1456314602
}
1456414603

14604+
function getLiteralType(value: string): StringLiteralType;
14605+
function getLiteralType(value: string | number | PseudoBigInt, enumId?: number, symbol?: Symbol): LiteralType;
1456514606
function getLiteralType(value: string | number | PseudoBigInt, enumId?: number, symbol?: Symbol) {
1456614607
// We store all literal types in a single map with keys of the form '#NNN' and '@SSS',
1456714608
// where NNN is the text representation of a numeric literal and SSS are the characters
@@ -17346,6 +17387,15 @@ namespace ts {
1734617387
}
1734717388
}
1734817389
}
17390+
else if (target.flags & TypeFlags.TemplateLiteral && source.flags & TypeFlags.StringLiteral) {
17391+
if (isPatternLiteralType(target)) {
17392+
// match all non-`string` segments
17393+
const result = inferLiteralsFromTemplateLiteralType(source as StringLiteralType, target as TemplateLiteralType);
17394+
if (result && every(result, (r, i) => isStringLiteralTypeValueParsableAsType(r, (target as TemplateLiteralType).types[i]))) {
17395+
return Ternary.True;
17396+
}
17397+
}
17398+
}
1734917399

1735017400
if (source.flags & TypeFlags.TypeVariable) {
1735117401
if (source.flags & TypeFlags.IndexedAccess && target.flags & TypeFlags.IndexedAccess) {
@@ -17386,8 +17436,15 @@ namespace ts {
1738617436
}
1738717437
}
1738817438
else if (source.flags & TypeFlags.TemplateLiteral) {
17439+
if (target.flags & TypeFlags.TemplateLiteral &&
17440+
(source as TemplateLiteralType).texts.length === (target as TemplateLiteralType).texts.length &&
17441+
(source as TemplateLiteralType).types.length === (target as TemplateLiteralType).types.length &&
17442+
every((source as TemplateLiteralType).texts, (t, i) => t === (target as TemplateLiteralType).texts[i]) &&
17443+
every((instantiateType(source, makeFunctionTypeMapper(reportUnreliableMarkers)) as TemplateLiteralType).types, (t, i) => !!((target as TemplateLiteralType).types[i].flags & (TypeFlags.Any | TypeFlags.String)) || !!isRelatedTo(t, (target as TemplateLiteralType).types[i], /*reportErrors*/ false))) {
17444+
return Ternary.True;
17445+
}
1738917446
const constraint = getBaseConstraintOfType(source);
17390-
if (constraint && (result = isRelatedTo(constraint, target, reportErrors))) {
17447+
if (constraint && constraint !== source && (result = isRelatedTo(constraint, target, reportErrors))) {
1739117448
resetErrorInfo(saveErrorInfo);
1739217449
return result;
1739317450
}
@@ -18308,12 +18365,12 @@ namespace ts {
1830818365

1830918366
if (type.flags & TypeFlags.Instantiable) {
1831018367
const constraint = getConstraintOfType(type);
18311-
if (constraint) {
18368+
if (constraint && constraint !== type) {
1831218369
return typeCouldHaveTopLevelSingletonTypes(constraint);
1831318370
}
1831418371
}
1831518372

18316-
return isUnitType(type);
18373+
return isUnitType(type) || !!(type.flags & TypeFlags.TemplateLiteral);
1831718374
}
1831818375

1831918376
function getBestMatchingType(source: Type, target: UnionOrIntersectionType, isRelatedTo = compareTypesAssignable) {
@@ -19693,6 +19750,63 @@ namespace ts {
1969319750
return !!(type.symbol && some(type.symbol.declarations, hasSkipDirectInferenceFlag));
1969419751
}
1969519752

19753+
function isValidBigIntString(s: string): boolean {
19754+
const scanner = createScanner(ScriptTarget.ESNext, /*skipTrivia*/ false);
19755+
let success = true;
19756+
scanner.setOnError(() => success = false);
19757+
scanner.setText(s + "n");
19758+
let result = scanner.scan();
19759+
if (result === SyntaxKind.MinusToken) {
19760+
result = scanner.scan();
19761+
}
19762+
const flags = scanner.getTokenFlags();
19763+
// validate that
19764+
// * scanning proceeded without error
19765+
// * a bigint can be scanned, and that when it is scanned, it is
19766+
// * the full length of the input string (so the scanner is one character beyond the augmented input length)
19767+
// * it does not contain a numeric seperator (the `BigInt` constructor does not accept a numeric seperator in its input)
19768+
return success && result === SyntaxKind.BigIntLiteral && scanner.getTextPos() === (s.length + 1) && !(flags & TokenFlags.ContainsSeparator);
19769+
}
19770+
19771+
function isStringLiteralTypeValueParsableAsType(s: StringLiteralType, target: Type): boolean {
19772+
if (target.flags & TypeFlags.Union) {
19773+
return !!forEachType(target, t => isStringLiteralTypeValueParsableAsType(s, t));
19774+
}
19775+
switch (target) {
19776+
case stringType: return true;
19777+
case numberType: return s.value !== "" && isFinite(+(s.value));
19778+
case bigintType: return s.value !== "" && isValidBigIntString(s.value);
19779+
// the next 4 should be handled in `getTemplateLiteralType`, as they are all exactly one value, but are here for completeness, just in case
19780+
// this function is ever used on types which don't come from template literal holes
19781+
case trueType: return s.value === "true";
19782+
case falseType: return s.value === "false";
19783+
case undefinedType: return s.value === "undefined";
19784+
case nullType: return s.value === "null";
19785+
default: return !!(target.flags & TypeFlags.Any);
19786+
}
19787+
}
19788+
19789+
function inferLiteralsFromTemplateLiteralType(source: StringLiteralType, target: TemplateLiteralType): StringLiteralType[] | undefined {
19790+
const value = source.value;
19791+
const texts = target.texts;
19792+
const lastIndex = texts.length - 1;
19793+
const startText = texts[0];
19794+
const endText = texts[lastIndex];
19795+
if (!(value.startsWith(startText) && value.endsWith(endText))) return undefined;
19796+
const matches = [];
19797+
const str = value.slice(startText.length, value.length - endText.length);
19798+
let pos = 0;
19799+
for (let i = 1; i < lastIndex; i++) {
19800+
const delim = texts[i];
19801+
const delimPos = delim.length > 0 ? str.indexOf(delim, pos) : pos < str.length ? pos + 1 : -1;
19802+
if (delimPos < 0) return undefined;
19803+
matches.push(getLiteralType(str.slice(pos, delimPos)));
19804+
pos = delimPos + delim.length;
19805+
}
19806+
matches.push(getLiteralType(str.slice(pos)));
19807+
return matches;
19808+
}
19809+
1969619810
function inferTypes(inferences: InferenceInfo[], originalSource: Type, originalTarget: Type, priority: InferencePriority = 0, contravariant = false) {
1969719811
let bivariant = false;
1969819812
let propagationType: Type;
@@ -20170,27 +20284,6 @@ namespace ts {
2017020284
}
2017120285
}
2017220286

20173-
function inferLiteralsFromTemplateLiteralType(source: StringLiteralType, target: TemplateLiteralType): Type[] | undefined {
20174-
const value = source.value;
20175-
const texts = target.texts;
20176-
const lastIndex = texts.length - 1;
20177-
const startText = texts[0];
20178-
const endText = texts[lastIndex];
20179-
if (!(value.startsWith(startText) && value.endsWith(endText))) return undefined;
20180-
const matches = [];
20181-
const str = value.slice(startText.length, value.length - endText.length);
20182-
let pos = 0;
20183-
for (let i = 1; i < lastIndex; i++) {
20184-
const delim = texts[i];
20185-
const delimPos = delim.length > 0 ? str.indexOf(delim, pos) : pos < str.length ? pos + 1 : -1;
20186-
if (delimPos < 0) return undefined;
20187-
matches.push(getLiteralType(str.slice(pos, delimPos)));
20188-
pos = delimPos + delim.length;
20189-
}
20190-
matches.push(getLiteralType(str.slice(pos)));
20191-
return matches;
20192-
}
20193-
2019420287
function inferFromObjectTypes(source: Type, target: Type) {
2019520288
if (getObjectFlags(source) & ObjectFlags.Reference && getObjectFlags(target) & ObjectFlags.Reference && (
2019620289
(<TypeReference>source).target === (<TypeReference>target).target || isArrayType(source) && isArrayType(target))) {
@@ -31688,9 +31781,6 @@ namespace ts {
3168831781
checkSourceElement(span.type);
3168931782
const type = getTypeFromTypeNode(span.type);
3169031783
checkTypeAssignableTo(type, templateConstraintType, span.type);
31691-
if (!everyType(type, t => !!(t.flags & TypeFlags.Literal) || isGenericIndexType(t))) {
31692-
error(span.type, Diagnostics.Template_literal_type_argument_0_is_not_literal_type_or_a_generic_type, typeToString(type));
31693-
}
3169431784
}
3169531785
getTypeFromTypeNode(node);
3169631786
}

src/compiler/diagnosticMessages.json

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3039,10 +3039,7 @@
30393039
"category": "Error",
30403040
"code": 2792
30413041
},
3042-
"Template literal type argument '{0}' is not literal type or a generic type.": {
3043-
"category": "Error",
3044-
"code": 2793
3045-
},
3042+
30463043
"Expected {0} arguments, but got {1}. Did you forget to include 'void' in your type argument to 'Promise'?": {
30473044
"category": "Error",
30483045
"code": 2794

src/compiler/types.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4925,7 +4925,7 @@ namespace ts {
49254925
NotPrimitiveUnion = Any | Unknown | Enum | Void | Never | StructuredOrInstantiable,
49264926
// The following flags are aggregated during union and intersection type construction
49274927
/* @internal */
4928-
IncludesMask = Any | Unknown | Primitive | Never | Object | Union | Intersection | NonPrimitive,
4928+
IncludesMask = Any | Unknown | Primitive | Never | Object | Union | Intersection | NonPrimitive | TemplateLiteral,
49294929
// The following flags are used for different purposes during union and intersection type construction
49304930
/* @internal */
49314931
IncludesStructuredOrInstantiable = TypeParameter,

tests/baselines/reference/constAssertions.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -303,7 +303,7 @@ declare function ff2<T extends string, U extends string>(x: T, y: U): `${T}-${U}
303303
declare const ts1: "foo-bar";
304304
declare const ts2: "foo-1" | "foo-0";
305305
declare const ts3: "top-left" | "top-right" | "bottom-left" | "bottom-right";
306-
declare function ff3(x: 'foo' | 'bar', y: object): string;
306+
declare function ff3(x: 'foo' | 'bar', y: object): `foo${string}` | `bar${string}`;
307307
declare type Action = "verify" | "write";
308308
declare type ContentMatch = "match" | "nonMatch";
309309
declare type Outcome = `${Action}_${ContentMatch}`;

tests/baselines/reference/constAssertions.types

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -441,13 +441,13 @@ const ts3 = ff2(!!true ? 'top' : 'bottom', !!true ? 'left' : 'right');
441441
>'right' : "right"
442442

443443
function ff3(x: 'foo' | 'bar', y: object) {
444-
>ff3 : (x: 'foo' | 'bar', y: object) => string
444+
>ff3 : (x: 'foo' | 'bar', y: object) => `foo${string}` | `bar${string}`
445445
>x : "foo" | "bar"
446446
>y : object
447447

448448
return `${x}${y}` as const;
449-
>`${x}${y}` as const : string
450-
>`${x}${y}` : string
449+
>`${x}${y}` as const : `foo${string}` | `bar${string}`
450+
>`${x}${y}` : `foo${string}` | `bar${string}`
451451
>x : "foo" | "bar"
452452
>y : object
453453
}

0 commit comments

Comments
 (0)