On this page
Abstract
Does a mapped type with an as clause, indexed at every call site, slow type-checking down compared with inferring the event name from a generic parameter? The same app was checked before and after the change with TypeScript 6 and TypeScript 7, on an Apple M3 Pro and an Intel Xeon, with a safety file of usages that must still fail to compile. On the Apple M3 Pro, check time fell by 52% with TypeScript 6 and by 57% with TypeScript 7, with the same type safety. The main limit: one app written for the experiment, measured with the earlier protocol, without a warm-up run or interleaving.
Key numbers
−52%
check time with TypeScript 6 after the fix
median check time, after vs before
Apple M3 Pro,
−57%
check time with TypeScript 7 after the fix
median check time, after vs before
Apple M3 Pro,
−65%
check time with TypeScript 7 after the fix, on one core
median check time, after vs before
Intel Xeon,
1Introduction
Typed event emitters are everywhere in TypeScript code: a map from event names to payload types, and an on method that looks up the payload for each name. Deriving the names with a mapped type and an as clause is concise and reads well, but the checker has to evaluate that type again at every call site that indexes it.
A cost paid per call site grows with the codebase and is paid on every type-check, locally and in CI. This experiment measures whether that cost is large enough to matter, and whether a simpler pattern removes it without weakening the types.
2Method
Subject
A small app written for the experiment, with many typed settings and listeners. In before/, event names come from a mapped type with an as clause, and the tsconfig has no include, so a generated file the app never imports is checked too. In after/, the event name is inferred from the generic parameter, and include is narrowed to src. safety.ts lists wrong usages that must still fail to compile with the fixed version.
Code: experiments/001-typed-events in the lab repository. Each run is tsc -p <project> --extendedDiagnostics.
Environment and protocol, per machine
Apple M3 Pro, 12 cores
- CPU
- Apple M3 Pro
- Cores
- 12
- RAM
- 36 GB
- OS
- Darwin 25.2.0
- Node.js
- 22.23.3
- Compilers
- TypeScript 6.0.3, TypeScript 7.0.2
Protocol
- Runs per condition
- 3
- Warm-up
- None
- Order
- In sequence (A A A B B B)
- Cooldown
- Not recorded
- Type safety
- Passed: invalid usages still fail to compile
Conditions
- before (TypeScript 6.0.3), 3 runs
- after (TypeScript 6.0.3), 3 runs
- before (TypeScript 7.0.2), 3 runs
- after (TypeScript 7.0.2), 3 runs
Intel Xeon, 1 core
- CPU
- Intel Xeon Processor @ 2.30GHz
- Cores
- 1
- RAM
- 4 GB
- OS
- Linux 6.18.44-fc-v230
- Node.js
- 22.22.2
- Compilers
- TypeScript 6.0.3, TypeScript 7.0.2
Protocol
- Runs per condition
- 3
- Warm-up
- None
- Order
- In sequence (A A A B B B)
- Cooldown
- Not recorded
- Type safety
- Passed: invalid usages still fail to compile
Conditions
- before (TypeScript 6.0.3), 3 runs
- after (TypeScript 6.0.3), 3 runs
- before (TypeScript 7.0.2), 3 runs
- after (TypeScript 7.0.2), 3 runs
Metrics
- Check time
- The "Check time" line printed by
tsc --extendedDiagnostics, in seconds: the time spent in the type checker only. - Total time
- The "Total time" line printed by
tsc --extendedDiagnostics, in seconds: parsing, binding and checking together. - Median
- The middle value of the measured runs, or the mean of the two middle values for an even count. Every reported figure is a median.
- Range
- The fastest and slowest measured runs, shown as whiskers on the charts.
- IQR
- Interquartile range, Q3 minus Q1, with quartiles by linear interpolation between order statistics.
- CV
- Coefficient of variation: sample standard deviation divided by the mean. Above 5%, the cause is investigated and logged.
- Cut
- One minus the ratio of the median check time after the fix to the median before it. Its range uses the fastest and slowest runs of each side.
3Results
Apple M3 Pro, 12 cores
Bar: median. Whisker: fastest to slowest run.
Data table
| Condition | Runs | Median | Min | Max | IQR | CV |
|---|---|---|---|---|---|---|
| before (TypeScript 6.0.3) | 3 | 0.890 s | 0.890 s | 0.910 s | 0.010 s | 1.3% |
| after (TypeScript 6.0.3) | 3 | 0.430 s | 0.420 s | 0.440 s | 0.010 s | 2.3% |
| before (TypeScript 7.0.2) | 3 | 0.330 s | 0.326 s | 0.330 s | 0.002 s | 0.7% |
| after (TypeScript 7.0.2) | 3 | 0.143 s | 0.117 s | 0.159 s | 0.021 s | 15.2% |
| Compiler | Cut | Conservative range |
|---|---|---|
| TypeScript 6 | −52% | 51% to 54% |
| TypeScript 7 | −57% | 51% to 65% |
| Condition | Runs | Median | Min | Max | IQR | CV | Total timemedian | Peak RSSmedian |
|---|---|---|---|---|---|---|---|---|
| before (TypeScript 6.0.3) | 3 | 0.890 s | 0.890 s | 0.910 s | 0.010 s | 1.3% | 1.10 s | 282 MiB |
| after (TypeScript 6.0.3) | 3 | 0.430 s | 0.420 s | 0.440 s | 0.010 s | 2.3% | 0.560 s | 225 MiB |
| before (TypeScript 7.0.2) | 3 | 0.330 s | 0.326 s | 0.330 s | 0.002 s | 0.7% | 0.349 s | 200 MiB |
| after (TypeScript 7.0.2) | 3 | 0.143 s | 0.117 s | 0.159 s | 0.021 s | 15.2% | 0.166 s | 112 MiB |
Compiler counters and reported memory
| Condition | Memory reportedby the compiler, median | Instantiations | Types | Files |
|---|---|---|---|---|
| before (TypeScript 6.0.3) | 170 MiB | 173,307 | 38,336 | 65 |
| after (TypeScript 6.0.3) | 124 MiB | 64,166 | 33,785 | 64 |
| before (TypeScript 7.0.2) | 89 MiB | 260,791 | 55,537 | 65 |
| after (TypeScript 7.0.2) | 70 MiB | 142,952 | 47,724 | 64 |
Intel Xeon, 1 core
Bar: median. Whisker: fastest to slowest run.
Data table
| Condition | Runs | Median | Min | Max | IQR | CV |
|---|---|---|---|---|---|---|
| before (TypeScript 6.0.3) | 3 | 3.55 s | 3.49 s | 3.59 s | 0.050 s | 1.4% |
| after (TypeScript 6.0.3) | 3 | 2.11 s | 2.07 s | 2.17 s | 0.050 s | 2.4% |
| before (TypeScript 7.0.2) | 3 | 0.927 s | 0.886 s | 0.932 s | 0.023 s | 2.8% |
| after (TypeScript 7.0.2) | 3 | 0.326 s | 0.303 s | 0.344 s | 0.020 s | 6.3% |
| Compiler | Cut | Conservative range |
|---|---|---|
| TypeScript 6 | −41% | 38% to 42% |
| TypeScript 7 | −65% | 61% to 67% |
| Condition | Runs | Median | Min | Max | IQR | CV | Total timemedian | Peak RSSmedian |
|---|---|---|---|---|---|---|---|---|
| before (TypeScript 6.0.3) | 3 | 3.55 s | 3.49 s | 3.59 s | 0.050 s | 1.4% | 4.53 s | n/a |
| after (TypeScript 6.0.3) | 3 | 2.11 s | 2.07 s | 2.17 s | 0.050 s | 2.4% | 2.76 s | n/a |
| before (TypeScript 7.0.2) | 3 | 0.927 s | 0.886 s | 0.932 s | 0.023 s | 2.8% | 1.01 s | n/a |
| after (TypeScript 7.0.2) | 3 | 0.326 s | 0.303 s | 0.344 s | 0.020 s | 6.3% | 0.386 s | n/a |
Compiler counters and reported memory
| Condition | Memory reportedby the compiler, median | Instantiations | Types | Files |
|---|---|---|---|---|
| before (TypeScript 6.0.3) | 176 MiB | 173,307 | 38,336 | 65 |
| after (TypeScript 6.0.3) | 114 MiB | 64,166 | 33,785 | 64 |
| before (TypeScript 7.0.2) | 88 MiB | 260,791 | 55,537 | 65 |
| after (TypeScript 7.0.2) | 70 MiB | 142,952 | 47,724 | 64 |
4Discussion
The fix pays off with both compilers. TypeScript 7 does not make the costly pattern harmless: it still evaluates the mapped type at each call site, so the simpler pattern keeps its advantage after a migration.
On the Apple M3 Pro the ranges of the two cuts overlap (51% to 54% with TypeScript 6, 51% to 65% with TypeScript 7), so this protocol cannot say which compiler gains more there. On the Intel Xeon they do not overlap (38% to 42% and 61% to 67%).
Lab #002 measures the other lever, the compiler itself, on a real library. The two are independent: a type pattern that is slow to check costs time with either compiler, and fixing it adds to the gain of the migration.
5Limitations and threats to validity
- Both machines use the earlier protocol: 3 runs per condition, no warm-up, runs in sequence. On the Apple M3 Pro, the runs of TypeScript 7 after the fix vary more than the others (0.117 s to 0.159 s, 15.2% coefficient of variation), hence the wider range on that figure.
- The two machines differ by CPU as well as core count: compare the before and after conditions within a machine.
- Results come from one machine per session: compare conditions within the same machine only.
6Conclusion and recommendations
Yes. On an Apple M3 Pro (12 cores), inferring the event name from the generic parameter cut check time by 52% with TypeScript 6 and by 57% with TypeScript 7 (51% to 65% from the fastest and slowest runs); on a 1-core Intel Xeon, by 41% and 65%. The safety check passed on both machines, so the type safety is the same. Narrowing include to src also stopped TypeScript from reading a generated file the app never imports.
A mapped type with an as clause is evaluated backwards at each indexed call site. If you can infer the key from a generic instead, the checker does far less work.
Recommendations
- Look for mapped types with an
asclause that are indexed at many call sites, and infer the key from a generic parameter instead. - Keep a safety file of usages that must still fail to compile, so the faster version is proven to be as strict.
- Check that
includecovers only the code the app compiles: here a generated file the app never imports was type-checked on every run.
7Reproduce
Node.js 22 or later and git. From the repository root, run the experiment, then recompute every statistic from the results files. New results land in experiments/001-typed-events/results/: open a pull request with yours to add a machine.
npm install && npm run lab:001node experiments/001-typed-events/analyze.mjs