Description
Bug Report
When using watch and template literal recursion, TSC tends to run out of memory. How soon depends on the platform and memory size, but on a powerful Silicon Mac , it takes 2-3 modifications to the code base. On a memory-limited Ubuntu VM, a single save after the initial compilation will error out.
🔎 Search Terms
watch
recursion
template literal types
memory
heap
🕗 Version & Regression Information
Tested with 4.4, 4.5, and nightly. Happens in all, consistently.
Tested on MacOS Silicon and Ubuntu.
Please keep and fill in the line that best applies:
- This is a crash
⏯ Playground Link
This uses tsc watch, so a link to the playground is not possible. I provide sample code below.
💻 Code
https://github.com/silviogutierrez/tsc-recursion-bug
Using node 14 and yarn.
To run into the issue:
git clone https://github.com/silviogutierrez/tsc-recursion-bug.git
yarn tsc --noEmit --watch
- Note the initial compilation is very fast.
- Open an editor and materially change
client/routes.tsx
. Change the name, etc. - Further compilations are slow, and eventually you'll get a massive out of memory error.
🙁 Actual behavior
Out of memory error with a long stack trace. Very little info on why.
🙂 Expected behavior
Quick compilation that takes about the same time as the initial compilation. Not only is it much slower when it does work, but it crashes eventually.
More background
The issue is caused by ExtractRouteParams in monoroutes.tsx
. If you replace the definition with:
type ExtractRouteParams<T> = any
The problem immediately goes away. Obviously this isn't what we want. You can review routes.tsx
to see the intent of this type. That is, to strongly type params between string and numbers.
Moreover, the problem is related to route
being a recursive function. See routes.tsx
for an example of how it's used to create sub routes that share paths.
Other things that "fix" the error:
- Eliminate
innerRoute
frommonoroutes.tsx
, limiting routes to a single level. - Make
ExtractRouteParams
less powerful. Make it only parse params instead of string/number. Like so:
type ExtractRouteParams<T> = string extends T
? Record<string, string | number>
: T extends `${infer _Start}<${infer Param}>/${infer Rest}`
? ExtractRouteParams<_Start> & {[k in Param]: string} & ExtractRouteParams<Rest>
: T extends `${infer _Start}<${infer Param}>`
? {[k in Param]: string}
: {};
Happy to provide more context. Normally I would say this is a real edge case, but the fact that it only affects watch
functionality and not the initial compile leads me to believe it needs to be addressed. Thanks for all the work on typescript!