Skip to content

Commit fc6f627

Browse files
committed
Allow bigint, number, null, and undefined in template holes
1 parent 6b0cf9a commit fc6f627

File tree

7 files changed

+1266
-13
lines changed

7 files changed

+1266
-13
lines changed

src/compiler/checker.ts

Lines changed: 77 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -755,7 +755,7 @@ namespace ts {
755755
const stringNumberSymbolType = getUnionType([stringType, numberType, esSymbolType]);
756756
const keyofConstraintType = keyofStringsOnly ? stringType : stringNumberSymbolType;
757757
const numberOrBigIntType = getUnionType([numberType, bigintType]);
758-
const templateConstraintType = getUnionType([stringType, numberType, booleanType, bigintType]);
758+
const templateConstraintType = getUnionType([stringType, numberType, booleanType, bigintType, nullType, undefinedType]) as UnionType;
759759

760760
const restrictiveMapper: TypeMapper = makeFunctionTypeMapper(t => t.flags & TypeFlags.TypeParameter ? getRestrictiveTypeParameter(<TypeParameter>t) : t);
761761
const permissiveMapper: TypeMapper = makeFunctionTypeMapper(t => t.flags & TypeFlags.TypeParameter ? wildcardType : t);
@@ -13173,6 +13173,30 @@ namespace ts {
1317313173
return true;
1317413174
}
1317513175

13176+
/**
13177+
* 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`
13178+
*/
13179+
function extractRedundantTemplateLiterals(types: Type[]): boolean {
13180+
let i = types.length;
13181+
const literals = filter(types, t => !!(t.flags & TypeFlags.StringLiteral));
13182+
while (i > 0) {
13183+
i--;
13184+
const t = types[i];
13185+
if (!(t.flags & TypeFlags.TemplateLiteral)) continue;
13186+
for (const t2 of literals) {
13187+
if (isTypeSubtypeOf(t2, t)) {
13188+
// eg, ``get${T}` & "getX"` is just `"getX"`
13189+
orderedRemoveItemAt(types, i);
13190+
break;
13191+
}
13192+
else if (isPatternLiteralType(t)) {
13193+
return true;
13194+
}
13195+
}
13196+
}
13197+
return false;
13198+
}
13199+
1317613200
function extractIrreducible(types: Type[], flag: TypeFlags) {
1317713201
if (every(types, t => !!(t.flags & TypeFlags.Union) && some((t as UnionType).types, tt => !!(tt.flags & flag)))) {
1317813202
for (let i = 0; i < types.length; i++) {
@@ -13321,7 +13345,12 @@ namespace ts {
1332113345
}
1332213346
}
1332313347
else {
13324-
result = createIntersectionType(typeSet, aliasSymbol, aliasTypeArguments);
13348+
if (includes & TypeFlags.TemplateLiteral && includes & TypeFlags.StringLiteral && extractRedundantTemplateLiterals(typeSet)) {
13349+
result = neverType;
13350+
}
13351+
else {
13352+
result = createIntersectionType(typeSet, aliasSymbol, aliasTypeArguments);
13353+
}
1332513354
}
1332613355
intersectionTypes.set(id, result);
1332713356
}
@@ -13484,12 +13513,12 @@ namespace ts {
1348413513
let text = texts[0];
1348513514
for (let i = 0; i < types.length; i++) {
1348613515
const t = types[i];
13487-
if (t.flags & TypeFlags.Literal) {
13516+
if (t.flags & (TypeFlags.Literal | TypeFlags.Null | TypeFlags.Undefined)) {
1348813517
const s = applyTemplateCasing(getTemplateStringForType(t) || "", casings[i]);
1348913518
text += s;
1349013519
text += texts[i + 1];
1349113520
}
13492-
else if (isGenericIndexType(t) || t === stringType) {
13521+
else if (isGenericIndexType(t) || templateConstraintType.types.indexOf(t) !== -1) {
1349313522
newTypes.push(t);
1349413523
newCasings.push(casings[i]);
1349513524
newTexts.push(text);
@@ -13516,6 +13545,8 @@ namespace ts {
1351613545
type.flags & TypeFlags.NumberLiteral ? "" + (<NumberLiteralType>type).value :
1351713546
type.flags & TypeFlags.BigIntLiteral ? pseudoBigIntToString((<BigIntLiteralType>type).value) :
1351813547
type.flags & TypeFlags.BooleanLiteral ? (<IntrinsicType>type).intrinsicName :
13548+
type.flags & TypeFlags.Null ? "null" :
13549+
type.flags & TypeFlags.Undefined ? "undefined" :
1351913550
undefined;
1352013551
}
1352113552

@@ -13754,7 +13785,7 @@ namespace ts {
1375413785
}
1375513786

1375613787
function isPatternLiteralType(type: Type) {
13757-
return !!(type.flags & TypeFlags.TemplateLiteral) && every((type as TemplateLiteralType).types, t => t === stringType);
13788+
return !!(type.flags & TypeFlags.TemplateLiteral) && every((type as TemplateLiteralType).types, t => templateConstraintType.types.indexOf(t) !== -1);
1375813789
}
1375913790

1376013791
function isGenericObjectType(type: Type): boolean {
@@ -14502,6 +14533,8 @@ namespace ts {
1450214533
return !!(type.flags & TypeFlags.Literal) && (<LiteralType>type).freshType === type;
1450314534
}
1450414535

14536+
function getLiteralType(value: string): StringLiteralType;
14537+
function getLiteralType(value: string | number | PseudoBigInt, enumId?: number, symbol?: Symbol): LiteralType;
1450514538
function getLiteralType(value: string | number | PseudoBigInt, enumId?: number, symbol?: Symbol) {
1450614539
// We store all literal types in a single map with keys of the form '#NNN' and '@SSS',
1450714540
// where NNN is the text representation of a numeric literal and SSS are the characters
@@ -17285,7 +17318,7 @@ namespace ts {
1728517318
if (isPatternLiteralType(target)) {
1728617319
// match all non-`string` segemnts
1728717320
const result = inferLiteralsFromTemplateLiteralType(source as StringLiteralType, target as TemplateLiteralType);
17288-
if (result) {
17321+
if (result && every(result, (r, i) => stringLiteralTypeParsesAsType(r, (target as TemplateLiteralType).types[i]))) {
1728917322
return Ternary.True;
1729017323
}
1729117324
}
@@ -19612,7 +19645,43 @@ namespace ts {
1961219645
return !!(type.symbol && some(type.symbol.declarations, hasSkipDirectInferenceFlag));
1961319646
}
1961419647

19615-
function inferLiteralsFromTemplateLiteralType(source: StringLiteralType, target: TemplateLiteralType): Type[] | undefined {
19648+
function isValidBigIntString(s: string): boolean {
19649+
const scanner = createScanner(ScriptTarget.ESNext, /*skipTrivia*/ false);
19650+
let success = true;
19651+
scanner.setOnError(() => success = false);
19652+
scanner.setText(s + "n");
19653+
let result = scanner.scan();
19654+
if (result === SyntaxKind.MinusToken) {
19655+
result = scanner.scan();
19656+
}
19657+
const flags = scanner.getTokenFlags();
19658+
// validate that
19659+
// * scanning proceeded without error
19660+
// * a bigint can be scanned, and that when it is scanned, it is
19661+
// * the full length of the input string (so the scanner is one character beyond the augmented input length)
19662+
// * it does not contain a numeric seperator (the `BigInt` constructor does not accept a numeric seperator in its input)
19663+
return success && result === SyntaxKind.BigIntLiteral && scanner.getTextPos() === (s.length + 1) && !(flags & TokenFlags.ContainsSeparator);
19664+
}
19665+
19666+
function stringLiteralTypeParsesAsType(s: StringLiteralType, target: Type): boolean {
19667+
if (target.flags & TypeFlags.Union) {
19668+
return !!forEachType(target, t => stringLiteralTypeParsesAsType(s, t));
19669+
}
19670+
switch (target) {
19671+
case stringType: return true;
19672+
case numberType: return s.value !== "" && isFinite(+(s.value));
19673+
case bigintType: return s.value !== "" && isValidBigIntString(s.value);
19674+
// the next 4 should be handled in `getTemplateLiteralType`, as they are all exactly one value, but are here for completeness, just in case
19675+
// this function is ever used on types which don't come from template literal holes
19676+
case trueType: return s.value === "true";
19677+
case falseType: return s.value === "false";
19678+
case undefinedType: return s.value === "undefined";
19679+
case nullType: return s.value === "null";
19680+
default: return false;
19681+
}
19682+
}
19683+
19684+
function inferLiteralsFromTemplateLiteralType(source: StringLiteralType, target: TemplateLiteralType): StringLiteralType[] | undefined {
1961619685
const value = source.value;
1961719686
const texts = target.texts;
1961819687
const lastIndex = texts.length - 1;
@@ -31593,7 +31662,7 @@ namespace ts {
3159331662
checkSourceElement(span.type);
3159431663
const type = getTypeFromTypeNode(span.type);
3159531664
checkTypeAssignableTo(type, templateConstraintType, span.type);
31596-
if (!everyType(type, t => !!(t.flags & TypeFlags.Literal) || isGenericIndexType(t) || t === stringType)) {
31665+
if (!everyType(type, t => !!(t.flags & TypeFlags.Literal) || isTypeAssignableTo(t, templateConstraintType))) {
3159731666
error(span.type, Diagnostics.Template_literal_type_argument_0_is_not_literal_type_or_a_generic_type, typeToString(type));
3159831667
}
3159931668
}

src/compiler/types.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4938,7 +4938,7 @@ namespace ts {
49384938
NotPrimitiveUnion = Any | Unknown | Enum | Void | Never | StructuredOrInstantiable,
49394939
// The following flags are aggregated during union and intersection type construction
49404940
/* @internal */
4941-
IncludesMask = Any | Unknown | Primitive | Never | Object | Union | Intersection | NonPrimitive,
4941+
IncludesMask = Any | Unknown | Primitive | Never | Object | Union | Intersection | NonPrimitive | TemplateLiteral,
49424942
// The following flags are used for different purposes during union and intersection type construction
49434943
/* @internal */
49444944
IncludesStructuredOrInstantiable = TypeParameter,

0 commit comments

Comments
 (0)