Repository navigation
Description
🔎 Search Terms
inference, deeply nested, no error, type evaluation, typebox, hkt
🕗 Version & Regression Information
Hi,
I've come across some unusual behavior where TypeScript seems to be skipping checks for certain kinds of computed types. This seems to be specific to deeply nested object types that are computed via intersection. I note that the language service is reporting the correct type information in editor, however the compiler seems to be skipping checks on the reported type. This behavior can be observed using the TypeBox library, specifically for object types at least 2 levels deep.
- This is the behavior in every version I tried (5.1.6 onwards)
⏯ Playground Link
💻 Code
import { Static, Type } from "@sinclair/typebox"
export type Input = Static<typeof Input>
export const Input = Type.Object({
level1: Type.Object({
level2: Type.Object({
foo: Type.String(),
})
})
})
export type Output = Static<typeof Output>
export const Output = Type.Object({
level1: Type.Object({
level2: Type.Object({
foo: Type.String(),
bar: Type.String(),
})
})
})
// The following functions should all report errors for the return value, however the first does not.
function problematicFunction1(ors: Input[]): Output[] {
return ors; // <-- this does not error
}
function problematicFunction2<T extends Output[]>(ors: Input[]): T {
return ors; // <-- ... but this does
}
function problematicFunction3(ors: (typeof Input.static)[]): Output[] {
return ors; // <-- ... and so does this
}🙁 Actual behavior
First function does not report an error
🙂 Expected behavior
All functions should report errors
Additional information about the issue
I get the feeling this may have something to do with TypeBox's implementation of Static<T> and where TypeScript may be implementing some optimizations around certain forms of computed / evaluated types (but not sure). TypeBox currently implements Static<T> as follows.
type Static<T extends TSchema, P extends unknown[] = []> = (T & { params: P; })["static"]By removing the intersection of P, TypeScript correctly asserts the problematicFunction1 return type.
type Static<T extends TSchema, P extends unknown[] = []> = T["static"] // this is fine
Activity
RyanCavanaugh commentedon Oct 17, 2023
Please inline the relevant parts of
typeboxsinclairzx81 commentedon Oct 17, 2023
Ryan Cavanaugh (@RyanCavanaugh) See link below for the relevant parts of TypeBox inlined
TypeScript Link Here
RyanCavanaugh commentedon Oct 17, 2023
WIP reduction
RyanCavanaugh commentedon Oct 17, 2023
As expected, we're hitting a depth limit
Source 236 is
Evaluate<T>165 is
Evaluate<T>222 is
Evaluate<T>236 is
Evaluate<T>Target 229 is
Evaluate<T>198 is
Evaluate<T>215 is
Evaluate<T>229 is ...
Evaluate<T>The source stack is
and the target stack is
Since each of these types are instantiations of the same symbol
Evaluate<T>, it appears that we might be stuck in an infinitely-expanding recursive type like this one, where each level of expansion creates new fresh types that seem like they might be worth looking into:So we need some way to tell apart OP's situation - where the types are actually converging, probably - from the infinite-descent case where they'll never converge.
Go 5 levels deep when determining if an object type is deeply nested
RyanCavanaugh commentedon Oct 17, 2023
I'm going to put up a PR to change the object type depth limit from 3 to 5 but it's almost certainly going to break someone else's performance or cause them to hit a depth limit which they previously didn't...
RyanCavanaugh commentedon Oct 17, 2023
The PR tanked xstate performance; we'll need something smarter.
Reduced and annotated example (apologies for renaming but these make more sense to me and avoid conflicting with certain terms we use internally):
This example defeats the existing "are we making progress" heuristics because of
When computing
Output.level1.level2, this is computingthis['objectProps']['objectProps']['objectProps'], which looks a heck of a lot like the infinite series ofnexts inRabbitHole.sinclairzx81 commentedon Oct 17, 2023
Ryan Cavanaugh (@RyanCavanaugh)
Hey no problem. Also, thank you for taking the time to investigate this issue (and for the test PR), that was very much appreciated. If there is anything that can be done library side to help work within TS depth limits here, please let me know.
fatcerberus commentedon Oct 18, 2023
Didn't Anders Hejlsberg (@ahejlsberg) recently author a PR with smarter logic for this that went beyond the usual "stop after 3 levels of
Foo<T>"?sinclairzx81 commentedon Oct 18, 2023
Related #55638
fatcerberus commentedon Oct 18, 2023
sinclairzx81 That was the PR I was thinking of, thanks!
webstrand commentedon Oct 18, 2023
Could we perhaps explicitly mark these cases instead, if detecting them automatically proves infeasible?
fatcerberus commentedon Oct 18, 2023
webstrand I think the halting problem goes both ways - if you can't prove that a program halts (or, equivalently, that a type is not infinitely recursive), then you also can't prove that it doesn't. So you'd have to rely on equally inaccurate heuristics to do that marking, no?
webstrand commentedon Oct 19, 2023
I mean an intrinsic
Converges<T>(or a keyword) which may loop infinitely if poorly written. I recognize that detecting these cases automatically may be impossible, but rather than an esoteric "you must write your recursive types in a particular pattern that typescript recognizes, but is only known to the few who frequent issues" mark it explicitly.ahejlsberg commentedon Oct 19, 2023
With #55638 we got pretty close to fixing the issue. However, use of the non-homomorphic
Pickand theEvaluateconditional causes the compiler to loose track of the symbol of the original literal type. Once that happens, all of the transformed types have the same symbol and we stop relating them after three levels. The code works if I remove the use ofEvaluateand change to a homomorphic definition ofPick:I'll keep thinking about this, but it's a really tricky issue to solve.
ahejlsberg commentedon Oct 21, 2023
Actually, it appears possible to fix this by improving our logic for tracking deeply nested types in relationships. I will put up a PR.
sinclairzx81 commentedon Oct 23, 2023
Anders Hejlsberg (@ahejlsberg) This PR is amazing. Thank you so much!