Skip to content

Type erasure changes directive-prologue semantics after type-only declarations #64033

Description

@magic-akari

🔎 Search Terms

directive prologue, type-only declaration, type alias, type erasure, string expression, use client, use strict, emit, transformer, erasableSyntaxOnly

🕗 Version & Regression Information

  • Reproduced with TypeScript 5.9.3 and 6.0.3.
  • Reproduced on TypeScript main at 5739027c9a7 (7.1.0-dev).
  • This is the behavior in every version I tried. I could not find an existing issue for this specific interaction after searching for the terms above.

⏯ Playground Link

https://www.typescriptlang.org/play/?#code/GYVwdgxgLglg9mABMOcAUBKRBvAUIgxKATwAcBTRAFUQF5EBnKAJxjAHMBufQgIhAaUIAGxjkwUXtwC+QA

💻 Code

function foo() {
    type T = string;
    "use client";
}

🙁 Actual behavior

TypeScript emits a bare string expression after removing the type alias:

function foo() {
    "use client";
}

The source string is preceded by a type-only declaration, but the emitted string is the first statement in the function body. It is therefore reinterpreted as part of the JavaScript Directive Prologue.

This changes the directive status of the string during type erasure. The same issue can affect use strict, which changes JavaScript runtime semantics, and implementation-defined directives such as use client or use server.

🙂 Expected behavior

A type-only declaration should terminate a Directive Prologue. Removing type-only syntax must not promote a following string expression into a directive. The emitted JavaScript should preserve the string as an ordinary expression statement, for example:

function foo() {
    ("use client");
}

Additional information about the issue

The parentheses do not change the value of the expression, but they prevent the statement from being parsed as a directive.

The purpose of this issue is to clarify whether a type-only declaration terminates a Directive Prologue or is transparent to it.

If TypeScript instead intends type-only declarations to be transparent to Directive Prologues, then source analysis should consistently recognize strings after those declarations as directives, and that TypeScript-specific extension should be documented. In either case, source interpretation and emitted JavaScript should agree.

ECMAScript context

ECMAScript defines a Directive Prologue as the longest initial sequence of string-literal expression statements in a function, script, or module body.

That rule establishes that JavaScript directives must be at the beginning and contiguous. A type alias is TypeScript-specific syntax, so the remaining question is how it participates in this sequence: whether it terminates the prologue or is transparent to it.

Ecosystem behavior

I compared parsers that support TypeScript or the equivalent Flow type-alias syntax. Their ASTs consistently classify the string after the type alias as a normal expression statement rather than a directive.

Tool Source AST / directive classification Output after removing types
TypeScript Its strict-mode prologue scan stops at the first non-directive statement, so the type alias prevents the later string from being found Bare 'use client';
Babel FunctionBody.directives is empty; the string remains an ExpressionStatement in body Bare 'use client';
@typescript-eslint/parser 8.68.0 ExpressionStatement.directive === undefined N/A
Yuku 0.9.1 ExpressionStatement.directive === null N/A
OXC 0.145.0 ExpressionStatement.directive === null Parenthesized ('use client');

This suggests a consistent source-level interpretation: the type-only declaration terminates the Directive Prologue.

Babel demonstrates that correct AST classification alone is not sufficient. Its TypeScript transform removes the type alias, and its generator then emits the now-leading string without parentheses, producing the same semantic change as TypeScript.

OXC preserves the AST distinction during output. Its code generator explicitly parenthesizes a leading string expression that is present in the statement list but absent from the directive list. For this input it emits:

function foo() {
    ("use client");
}

Either interpretation could be specified for TypeScript syntax, but the parser/binder interpretation and emitted JavaScript should not disagree.

Given the consistent ecosystem AST behavior and TypeScript's own contiguous prologue scan, I think type-only declarations should terminate the Directive Prologue, and type erasure should preserve that boundary by parenthesizing a following string expression when necessary.

Metadata

Metadata

Assignees

No one assigned

    Labels

    Not a DefectThis behavior is one of several equally-correct options

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions