microsoft / TypeScript

TypeScript is a superset of JavaScript that compiles to clean JavaScript output.
https://www.typescriptlang.org
Apache License 2.0
99.36k stars 12.31k forks source link

Inconsistent order of properties inferred from a generic type #46633

Open dko-slapdash opened 2 years ago

dko-slapdash commented 2 years ago

Bug Report

šŸ”Ž Search Terms

inconsistent properties order fields order

šŸ•— Version & Regression Information

4.4.4

Description

We noticed that the resulting .d.ts sometimes mention the properties of some types in random order, and this order changes every time a tsc --watch build unfreezes (i.e. it's not stable). Our build system consumes tsc output from multiple monorepo projects and pipes it to other different tools, so a change in a .d.ts file whilst in practice it should've not been changed triggers some excess rebuilds.

The effect is hard to reproduce (and especially hard to reproduce in a sandbox), so I'll try my best to provide all the info I have collected having low hopes. (But my only hope is that someone from TS engineers has a quick idea on where the reordering might come from.)

So this is the correct order of properties in the inferred type as shown by VSCode (externalID - assignee - completed):

image

And this is what I see in *.d.ts file during the initial build with --watch (incorrect order, externalID - name - dueAt):

image

This is what's there after a watch-build unfroze when I changed some unrelated file (another incorrect order, externalID - name - assignee, the 3rd variant):

image

Here is an extraction from the source code; I can't unfortunately sandbox it, so trying my best (notice IOValidatedFunc):

export default function IO<TInputLax extends object, TOutputLax extends object>(
  name: string,
  InputSchema: InputStruct<TInputLax>,
  OutputSchema: OutputStruct<TOutputLax>
) {
  function wrapper<TRest extends any[]>(
    func: IOPassedFunc<PartialToUndefined<TInputLax>, TOutputLax, TRest>
  ): IOValidatedFunc<TInputLax, TOutputLax, TRest> { ... }
  ...
  return wrapper;
}

...

// Superstruct library; it provides the correct TS typing from a validators structure.
UpdateTaskIO = IO(
  "UpdateTaskIO",
  object({
    externalID: string(),
    assignee: optional(nullable(string())),
    completed: optional(boolean()),
    dueAt: optional(nullable(JsonDate())),
    memberships: optional(
      array(
        object({
          project: string(),
          section: optional(string()),
        })
      )
    ),
    name: optional(trimmed(string())),
    notes: optional(string()),
    tags: optional(array(string())),
  }),
  NodeOutputSchema
);

...

override run = UpdateTaskIO(async (input) => { ... });

šŸ™ Actual behavior

The order of fields in *.d.ts file surprisingly changes when watch-building an unrelated file modification.

šŸ™‚ Expected behavior

*.d.ts content keeps unchanged if the source code is unchanged.

dko-slapdash commented 2 years ago

Several more examples (this time - for literal type alternatives).

Such reordering happens all the time, randomly.

image (4)