Skip to content

Better support for NoSubstitutionTemplateLiteral in checker #31548

@IllusionMH

Description

@IllusionMH

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

Example

Result:

const expr = '1';

enum Example {
    a = 'a',
    b = `b`, // Works
    c = `c` + `c`, // Works
    d = `a${expr}b` // Still error
}

Related functions:

  1. isStringConcatExpression (in checker.ts)
  2. isLiteralEnumMember (in checker.ts)
  3. getEnumKind (in checker.ts)
  4. evaluate (inside of computeConstanValue, in checker.ts)

2. Support template literals in enum properties access

Example

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:

  1. isConstantMemberAccess (in checker.ts)
  2. checkIndexedAccess (in checker.ts)

3. Support template literals in switch statements with typeof condition

Example

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:

  1. getSwitchClauseTypeOfWitnesses (in checker.ts)

4. Support template literals in const initializers inside an ambient declarations

Example

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:

  1. isStringOrNumberLiteralExpression (in checker.ts)

5. Support template literals in control flow checks

Example

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:

  1. getAccessedPropertyName (in checker.ts)
  2. isNarrowableReference (in binder.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:

  1. isLiteralComputedPropertyDeclarationName (in utilities.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:

  1. isExpressionNode (in utilities.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).

Example

Result:

let a: import(`test`); // OK
import b = require(`test`);  // OK

Related functions:

  1. isLiteralImportTypeNode (in utilities.ts)
  2. tryGetImportFromModuleSpecifier (in utilities.ts)
  3. checkExternalImportOrExportDeclaration (in checker.ts)
  4. getExternalModuleFileFromDeclaration (in checker.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

  1. What would be preferred way to check kind of node. isStringLiteralLike or (node.kind === SyntaxKind.StringLiteral || node.kind === SyntaxKind.NoSubstitutionTemplateLiteral)? Later won't look great in long && or || expressions.
  2. 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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    BugA bug in TypeScript

    Type

    No type

    Projects

    No projects

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions