Skip to content

TS 7: declaration emit prints union members in a different order from run to run #64589

Description

🔎 Search Terms

declaration emit union order, nondeterministic declaration output, unstable type ordering, CompareTypes type id, stableTypeOrdering, d.ts differs between runs

🕗 Version & Regression Information

  • This changed between versions 6.0.3 and 7.0.2

Still present in 7.1.0-dev.20261002.1. 6.0.3 prints one order in 30 of 30 runs, with and without --stableTypeOrdering.

⏯ Playground Link

No response (the bug is a difference between two runs of the compiler)

💻 Code

// index.ts
interface Endpoint<Name, Params, Middleware, Services> {
  readonly name: Name;
  readonly params: Params;
  readonly middleware: Middleware;
  readonly services: Services;
}

type Same<A, R> = R;

type AddMiddleware<E, I> =
  E extends Endpoint<infer N, infer P, infer M, infer S>
    ? Endpoint<N, P, M | I, Same<I, S>>
    : never;

interface Group<Endpoints> {
  add<N extends string, P = never>(
    name: N,
    params?: P,
  ): Group<Endpoints | Endpoint<N, P, never, never>>;
  middleware<I>(id: I): Group<AddMiddleware<Endpoints, I>>;
}

declare const group: Group<never>;

export const result = group.add("a", { id: "" }).add("c").middleware("m" as const);
for i in $(seq 1 60); do
  tsc index.ts --declaration --emitDeclarationOnly --strict --outDir out --singleThreaded
  grep -o 'Endpoint<"."' out/index.d.ts | tr -d '\n'; echo
done | sort | uniq -c

🙁 Actual behavior

The same file, built 60 times, gives two different index.d.ts files. They differ only in the order of the two members of the union:

7.0.2                   49 Endpoint<"a"Endpoint<"c"    11 Endpoint<"c"Endpoint<"a"
7.1.0-dev.20261002.1    53 Endpoint<"a"Endpoint<"c"     7 Endpoint<"c"Endpoint<"a"
// one run
export declare const result: Group<Endpoint<"a", { id: string; }, "m", never> | Endpoint<"c", never, "m", never>>;
// another run
export declare const result: Group<Endpoint<"c", never, "m", never> | Endpoint<"a", { id: string; }, "m", never>>;

This is with --singleThreaded. Without it the output also varies (3 of 40 runs).

🙂 Expected behavior

The same bytes on every run. The 6.0 release notes say that 7.0 sorts types by content so that the printed order does not depend on the order of checking.

Additional information about the issue

  • The alias in the last type argument matters. With Endpoint<N, P, M | I, S> in place of Endpoint<N, P, M | I, Same<I, S>> the output was the same in 60 of 60 runs.
  • CompareTypes in internal/checker/utilities.go ends with return int(t1.id) - int(t2.id). My guess is that these two instantiations reach that fallback, and that IDs are not assigned in one order from run to run.
  • Why it matters under tsc -b: a library whose declarations hold such a union gets a different .d.ts from a clean build with no source change, and every project that references it is checked again. In our repository 10 of 946 declaration files change between two clean builds (they hold unions of instantiations of Effect's HttpApiEndpoint), which makes a restored build cache close to useless in CI.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Labels

BugA bug in TypeScript

Type

No type

Projects

No projects

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions