Skip to content

Inconsistent order of properties inferred from a generic type #46633

@dko-slapdash

Description

@dko-slapdash

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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    BugA bug in TypeScriptDomain: Declaration EmitThe issue relates to the emission of d.ts files

    Type

    No type

    Projects

    No projects

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions