🔎 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.
🔎 Search Terms
declaration emit union order, nondeterministic declaration output, unstable type ordering, CompareTypes type id, stableTypeOrdering, d.ts differs between runs
🕗 Version & Regression Information
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
🙁 Actual behavior
The same file, built 60 times, gives two different
index.d.tsfiles. They differ only in the order of the two members of the union: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
Endpoint<N, P, M | I, S>in place ofEndpoint<N, P, M | I, Same<I, S>>the output was the same in 60 of 60 runs.CompareTypesininternal/checker/utilities.goends withreturn 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.tsc -b: a library whose declarations hold such a union gets a different.d.tsfrom 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'sHttpApiEndpoint), which makes a restored build cache close to useless in CI.