-
Notifications
You must be signed in to change notification settings - Fork 12.8k
TypeScript ignoring checks for deeply nested computed object types #56138
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
Comments
Please inline the relevant parts of |
@RyanCavanaugh See link below for the relevant parts of TypeBox inlined |
WIP reduction 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(),
})
})
})
function problematicFunction1(ors: Input): Output {
return ors; // <-- this does not error
}
function f() {
problematicFunction1(null as any);
}
f();
export type Evaluate<T> = T extends infer O ? { [K in keyof O]: O[K] } : never;
export interface TSchema {
params: unknown[];
static: unknown;
}
interface HasStatic { static: unknown }
interface HasParams { params: unknown[] }
type RecordOfHasStatics = Record<string, HasStatic>;
export type PropertiesReduce<T extends RecordOfHasStatics, P extends unknown[]> = Evaluate<{ [K in keyof T]: Static<T[K], P> }>;
export interface TObject<T extends RecordOfHasStatics> extends HasParams {
static: PropertiesReduce<T, this['params']>;
properties: T;
}
export type Static<T extends HasStatic, P extends unknown[] = []> = (T & { params: P; })['static']
declare namespace Type {
function Object<T extends RecordOfHasStatics>(object: T): TObject<T>;
function String(): TSchema & { static: string };;
} |
As expected, we're hitting a depth limit
Source 236 is Target 229 is The source stack is { level1: { level2: { foo: string; }; }; } -> { level2: { foo: string; }; } -> { foo: string; } and the target stack is { level1: { level2: { foo: string; bar: string; }; }; } -> { level2: { foo: string; bar: string; }; } -> { foo: string; bar: string; } Since each of these types are instantiations of the same symbol type RabbitHole<T, K> = {
next: RabbitHole<T, { obj: K }>
}
declare let x: RabbitHole<string, number>;
let y = x.next.next.next.next.next.next.next; 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. |
Fixes microsoft#56138, but at what cost...
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... |
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): type Input = GetType<typeof Input, []>
const Input = MakeObject({
level1: MakeObject({
level2: MakeObject({
foo: MakeString(),
})
})
});
type Output = GetType<typeof Output, []>;
const Output = MakeObject({
level1: MakeObject({
level2: MakeObject({
foo: MakeString(),
bar: MakeString(),
})
})
});
function problematicFunction1(ors: Input): Output {
// Should error
return ors;
}
// Representational types have a 'myType' field that represents
// the TypeScript type of the object, e.g.: MakeString below
interface HasType { myType: unknown }
declare function MakeString(): { myType: string };
// Reads the 'myType' field out of an object. 'P' exists for
// reverse inference purposes when dealing with object types (see MakeObject below)
type GetType<T extends HasType, P> = (T & { objectProps: P; })['myType'];
// Mapped version of above
type GetTypeMapped<T extends RecordOfTypes, P> = { [K in keyof T]: GetType<T[K], P> };
// Creates an object type
declare function MakeObject<T extends RecordOfTypes>(object: T): TObject<T>;
interface TObject<T extends RecordOfTypes> {
objectProps: unknown;
myType: GetTypeMapped<T, this['objectProps']>;
}
// Any dictionary from string -> statically-typed type
type RecordOfTypes = Record<string, HasType>; This example defeats the existing "are we making progress" heuristics because of
When computing |
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. |
Didn't @ahejlsberg recently author a PR with smarter logic for this that went beyond the usual "stop after 3 levels of |
Related #55638 |
@sinclairzx81 That was the PR I was thinking of, thanks! |
Could we perhaps explicitly mark these cases instead, if detecting them automatically proves infeasible? |
@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? |
I mean an intrinsic |
With #55638 we got pretty close to fixing the issue. However, use of the non-homomorphic type Pick<T, K extends keyof T> = { [P in keyof T as P & K]: T[P] }; I'll keep thinking about this, but it's a really tricky issue to solve. |
Actually, it appears possible to fix this by improving our logic for tracking deeply nested types in relationships. I will put up a PR. |
@ahejlsberg This PR is amazing. Thank you so much! |
🔎 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.
⏯ Playground Link
TypeScript Link Here
💻 Code
🙁 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 implementsStatic<T>
as follows.By removing the intersection of P, TypeScript correctly asserts the
problematicFunction1
return type.The text was updated successfully, but these errors were encountered: