Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Inlining Pick and Partial instead of using the builtins seems to result in less type instantiations #56017

Closed
1 task done
jussisaurio opened this issue Oct 7, 2023 · 3 comments

Comments

@jussisaurio
Copy link

jussisaurio commented Oct 7, 2023

Acknowledgement

  • I acknowledge that issues using this template may be closed without further explanation at the maintainer's discretion.

Comment

Apologies for using the Issue: Other template, I genuinely do not know if this is a bug or not.

I'm currently investigating a TS compilation speed improvement in Zod (see relevant PR here)

It seems as though replacing usages of the builtin Pick and Partial with inlined, functionally equivalent types results in less type instantiations, which, when the delta is large, starts to affect typechecking performance.

Builtins:

// This utility is the same in both versions
type requiredKeys<T extends object> = {
  [k in keyof T]: undefined extends T[k] ? never : k;
}[keyof T];
// This uses native Pick and Partial
export type addQuestionMarks<
  T extends object,
  R extends keyof T = requiredKeys<T>
> = Pick<Required<T>, R> & Partial<T>;

Inlined:

// This utility is the same in both versions
type requiredKeys<T extends object> = {
  [k in keyof T]: undefined extends T[k] ? never : k;
}[keyof T];
// This uses inlined Pick and Partial
export type addQuestionMarks<
  T extends object,
  R extends keyof T = requiredKeys<T>
> = { [K in R]: Required<T>[K] } & {
  [K in keyof T]?: T[K] | undefined;
};

Minimal reproduction repository

Compilation output in the minimal repro repo using native Pick and Partial:

% npm run compile-native

> ts-pick-partial-comparison@1.0.0 compile-native
> tsc -p nativePickPartial/tsconfig.json --noEmit --incremental false --extendedDiagnostics

Files:                         14
Lines of Library:            7008
Lines of Definitions:           0
Lines of TypeScript:           20
Lines of JavaScript:            0
Lines of JSON:                  0
Lines of Other:                 0
Identifiers:                 7773
Symbols:                     5307
Types:                       2027
Instantiations:               566
Memory used:               28688K
Assignability cache size:     144
Identity cache size:            0
Subtype cache size:             0
Strict subtype cache size:      0
I/O Read time:              0.00s
Parse time:                 0.04s
ResolveLibrary time:        0.00s
Program time:               0.05s
Bind time:                  0.01s
Check time:                 0.07s
printTime time:             0.00s
Emit time:                  0.00s
Total time:                 0.13s

Compilation output in the minimal repro repo using inlined Pick and Partial:

% npm run compile-mapped

> ts-pick-partial-comparison@1.0.0 compile-mapped
> tsc -p manualMappedTypes/tsconfig.json --noEmit --incremental false --extendedDiagnostics

Files:                         14
Lines of Library:            7008
Lines of Definitions:           0
Lines of TypeScript:           22
Lines of JavaScript:            0
Lines of JSON:                  0
Lines of Other:                 0
Identifiers:                 7776
Symbols:                     5311
Types:                       2028
Instantiations:               543
Memory used:               28733K
Assignability cache size:     155
Identity cache size:            0
Subtype cache size:             0
Strict subtype cache size:      0
I/O Read time:              0.00s
Parse time:                 0.04s
ResolveLibrary time:        0.00s
Program time:               0.05s
Bind time:                  0.01s
Check time:                 0.06s
printTime time:             0.00s
Emit time:                  0.00s
Total time:                 0.13s

In this minimal comparison, the difference is small but it's there. In the case of bigger projects using complex types via libraries like Zod, the difference seems to balloon substantially.

The main goal of this issue, I guess, is to find out the following:

  1. Is this comparison fair, i.e. are the builtin and the inlined versions infact somehow different, and
  2. If they are equivalent, why does the inlined version result in less instantiations?

System:

% npx envinfo

  System:
    OS: macOS 13.5.2
    CPU: (10) arm64 Apple M1 Max
  Binaries:
    Node: 20.5.1 - ~/Library/Caches/fnm_multishells/79183_1696663768732/bin/node
    npm: 9.8.0 - ~/Library/Caches/fnm_multishells/79183_1696663768732/bin/npm

% npm ls typescript
└── typescript@5.2.2
@fatcerberus
Copy link

The first question to ask is: does the inlined version behave the same when invoked on union types? My hunch is that the difference is down to Partial (and maybe Pick?) being distributive, i.e. Partial<A | B> is evaluated as Partial<A> | Partial<B>

@RyanCavanaugh
Copy link
Member

More instantations is expected; you can think of an instantiation sort of like resolving a layer of indirection, and aliases are definitely a manifest level of indirection.

@jussisaurio
Copy link
Author

Thanks a lot, that's insightful. My understanding of tsc internals is, let's say, limited.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

3 participants