-
Notifications
You must be signed in to change notification settings - Fork 13.1k
Description
Search Terms
template enum, template discriminated, template literals
Suggestion
Following #31042 (comment)
I've reviewed functions that has checks for StringLiterals, but checks for NoSubstitutionTemplateLiteral are missing or logic differs.
I've prepared code for all places below (except "Out of scope" sections) and most of the tests cases are ready for PR.
Could you please check cases below and approve proposed changes?
Please let me know if you have other examples where template literals behaves differently.
1. Support template literals in enum declaration
Original issue: #30962
Original PR: #31042
Result:
const expr = '1';
enum Example {
a = 'a',
b = `b`, // Works
c = `c` + `c`, // Works
d = `a${expr}b` // Still error
}Related functions:
isStringConcatExpression(inchecker.ts)isLiteralEnumMember(inchecker.ts)getEnumKind(inchecker.ts)evaluate(inside ofcomputeConstanValue, inchecker.ts)
2. Support template literals in enum properties access
Result:
const enum Test {
a = 1,
b = 2,
c = 3
}
const enum Test {
d = Test.a, // OK, Test.d = 1
e = Test['b'], // OK, Test.e = 2
f = Test[`c`] // OK, Test.f = 3
}
const a = Test.a; // OK, typeof a == Test
const b = Test['b']; // OK, typeof b == Test
const c = Test[`c`]; // OK, typeof c == Test
enum Example {
a = 10,
b = Example['a'], // OK, Example.b = 10
c = Example[`a`] // OK, Example.c = 10 (correct value now)
}Related functions:
isConstantMemberAccess(inchecker.ts)checkIndexedAccess(inchecker.ts)
3. Support template literals in switch statements with typeof condition
Result:
function errorOnValue(val: never): never {
throw new Error(`Unexpected value: ${val}`);
}
function test(value: number | string) {
switch (typeof value) {
case "number":
return 'num';
case `string`:
return 'str';
default:
return errorOnValue(value); // value will have type `never` and no error here
}
}Related functions:
getSwitchClauseTypeOfWitnesses(inchecker.ts)
4. Support template literals in const initializers inside an ambient declarations
Result:
declare module Example {
enum Test {
a = '1',
b = '2',
c = '3'
}
export const a = 'string';
export const b = `template`; // OK
export const c = Test.a;
export const d = Test['b'];
export const e = Test[`c`]; // OK
}Related functions:
isStringOrNumberLiteralExpression(inchecker.ts)
5. Support template literals in control flow checks
Result:
type T1 = { kind: 'A', a: number };
type T2 = { kind: 'B', b: string };
type TUnion = T1 | T2;
function verifyNever(val: never): never {
throw new Error(`Unexpected value: ${val}`);
}
function Example1(val: TUnion) {
switch (val[`kind`]) {
case 'A':
return val.a; // OK, val: T1
case 'B':
return val.b; // OK, val: T2
default:
return verifyNever(val); // OK, val: never
}
}
function Example2(val: TUnion) {
if (val[`kind`] === 'B') {
return val.b; // OK, val: T2
} else {
return val.a; // OK, val: T1
}
}Related functions:
getAccessedPropertyName(inchecker.ts)isNarrowableReference(inbinder.ts)
6. Unify symbols for computed properties
I've noticed difference only in generated symbols for computed properties, that will produce similar list of symbols as regular string literals.
Result:
class C {
>C : Symbol(C, Decl(computedPropertyNames13_ES6.ts, 2, 11))
static [""]() { }
>[""] : Symbol(C[""], Decl(computedPropertyNames13_ES6.ts, 8, 14))
>"" : Symbol(C[""], Decl(computedPropertyNames13_ES6.ts, 8, 14))
[`hello bye`]() { }
>[`hello bye`] : Symbol(C[`hello bye`], Decl(computedPropertyNames13_ES6.ts, 12, 28))
+>`hello bye` : Symbol(C[`hello bye`], Decl(computedPropertyNames13_ES6.ts, 12, 28))
}
Related functions:
isLiteralComputedPropertyDeclarationName(inutilities.ts)
7. Unify isExpressionNode checks
While both StringLiteral and NoSubstitutionTemplateLiteral kinds are checked inside this function - later one is always considered as expression.
Unified checks will remove additional type(?) line and make output similar to regular strings.
Result:
const x: `foo` = "foo";
>x : "foo"
->`foo` : "foo"
>"foo" : "foo"
Which will be similar to
const y: 'bar' = "bar";
>y : "bar"
>"bar" : "bar"
Related functions:
isExpressionNode(inutilities.ts)
8. Optional suggestion: Support template literals in let a: import(`test`) and import b = require(`test`)
These are two cases when TS correctly builds AST and can support template literals without updates to parser (see "Out of scope" section with examples of unexpected AST).
Result:
let a: import(`test`); // OK
import b = require(`test`); // OKRelated functions:
isLiteralImportTypeNode(inutilities.ts)tryGetImportFromModuleSpecifier(inutilities.ts)checkExternalImportOrExportDeclaration(inchecker.ts)getExternalModuleFileFromDeclaration(inchecker.ts)
Out of scope
Supporting template literals in import statements. Related issue: #29318
TS has unexpected (at least for me) AST built for cases like
declare module `*.tpl` {}
let a: Promise<`test`>;Related issue: #29331
I would like to leave these cases out of scope for new PR because these changes are not in checker level.
Few implementation questions
- What would be preferred way to check kind of node.
isStringLiteralLikeor(node.kind === SyntaxKind.StringLiteral || node.kind === SyntaxKind.NoSubstitutionTemplateLiteral)? Later won't look great in long&&or||expressions. - Is it possible to update text of existing error message (if it is used only in one place that will be updated) or I should create new message and existing one will be abandoned?
Checklist
My suggestion meets these guidelines:
- This wouldn't be a breaking change in existing TypeScript/JavaScript code
- This wouldn't change the runtime behavior of existing JavaScript code
- This could be implemented without emitting different JS based on the types of the expressions
- This isn't a runtime feature (e.g. library functionality, non-ECMAScript syntax with JavaScript output, etc.)
- This feature would agree with the rest of TypeScript's Design Goals.