Reputation: 7342
I have a recursive generic type definition that errors when it's used as an overload, but not when it's either not overloaded, or when it's not used inside another generic.
type JTDDataDef<S, D extends Record<string, unknown>> =
| (// ref
S extends { ref: string }
? JTDDataDef<D[S["ref"]], D>
: // type
S extends { type: "string" }
? string
: // properties
S extends {
properties: Record<string, unknown>
}
? { -readonly [K in keyof S["properties"]]-?: JTDDataDef<S["properties"][K], D> }
:
never)
| (S extends { nullable: true } ? null : never)
type JTDDataType<S> = S extends { definitions: Record<string, unknown> }
? JTDDataDef<S, S["definitions"]>
: JTDDataDef<S, Record<string, never>>
interface ValidateFunction<T> {
(data: unknown): data is T
}
function compile1<S>(schema: S): ValidateFunction<JTDDataType<S>>
function compile1<T>(schema: unknown): ValidateFunction<T> {
return (() => true) as unknown as ValidateFunction<T>;
}
function compile2<S>(schema: S): ValidateFunction<JTDDataType<S>> {
return (() => true) as unknown as ValidateFunction<JTDDataType<S>>;
}
function compile3<S>(schema: S): (data: unknown) => data is JTDDataType<S>
function compile3<T>(schema: unknown): (data: unknown) => data is T {
return (() => true) as unknown as (data: unknown) => data is T;
}
an error is thrown at the overload of compile1
saying
Type instantiation is excessively deep and possibly infinite.(2589)
However, no errors are thrown for compile2
or compile3
.
My guess is that the type is possibly infinite because of the recursion with ref
. However, typescript seems to actually handle the type just fine, e.g. if I define an infinite recursive type:
const llSchema = {
definitions: {
node: {
properties: {
val: { type: "string" },
next: {
ref: "node",
nullable: true
}
}
}
},
ref: "node",
nullable: true,
} as const;
const isLinkedList = compile1(llSchema);
const list: unknown = null;
type LinkedList = { val: string; next: LinkedList; } | null;
function getNextVal(data: LinkedList): string | null {
return data && data.next && data.next.val;
}
if (isLinkedList(list)) {
const val = list && list.val;
const nextVal = getNextVal(list);
}
typescript is actually okay with it, and typechecks it appropriately, even for the overloaded function that throws the error. I want to understand why it's throwing the error particularly for the overload signature, and if there's a way to no throw an error besides // @ts-expect-error
which seems like a heavy hammer in this case.
Upvotes: 1
Views: 191
Reputation: 33041
Keep in mind, that overloading - is function intersection.
Fix for compile1
type JTDDataDef<S, D extends Record<string, unknown>> =
| (// ref
S extends { ref: string }
? JTDDataDef<D[S["ref"]], D>
: // type
S extends { type: "string" }
? string
: // properties
S extends {
properties: Record<string, unknown>
}
? { -readonly [K in keyof S["properties"]]-?: JTDDataDef<S["properties"][K], D> }
:
never)
| (S extends { nullable: true } ? null : never)
type JTDDataType<S> = S extends { definitions: Record<string, unknown> }
? JTDDataDef<S, S["definitions"]>
: JTDDataDef<S, Record<string, never>>
interface ValidateFunction<T> {
(data: unknown): data is T
}
type O<S> = ValidateFunction<JTDDataType<S>>
type Overloading =
& (<S>(schema: JTDDataType<S>) => ValidateFunction<JTDDataType<S>>)
& (<T>(schema: T) => ValidateFunction<T>)
const compile1: Overloading = <T,>(schema: T): ValidateFunction<T> => {
return (() => true) as unknown as ValidateFunction<T>;
}
const llSchema = {
definitions: {
node: {
properties: {
val: { type: "string" },
next: {
ref: "node",
nullable: true
}
}
}
},
ref: "node",
nullable: true,
} as const;
const isLinkedList = compile1(llSchema);
function compile2<S>(schema: S): ValidateFunction<JTDDataType<S>> {
return (() => true) as unknown as ValidateFunction<JTDDataType<S>>;
}
//new error here
function compile3<S>(schema: S): (data: unknown) => data is JTDDataType<S>
function compile3<T>(schema: unknown): (data: unknown) => data is T {
return (() => true) as unknown as (data: unknown) => data is T;
}
As you might have noticed, I did not change compile3
, however I got an error here.
I think this is because when you have your first deep recursion error, TS has stopped and did not check compile2
and compile3
.
But this is only my assumptions.
Let's fix compile3
:
type Overloading2 =
& (<S>(schema: S) => (data: unknown) => data is JTDDataType<S>)
& (<T>(schema: unknown) => (data: unknown) => data is T)
const compile3: Overloading2 = <T,>(schema: unknown): (data: unknown) => data is T => {
return (() => true) as unknown as (data: unknown) => data is T;
}
Whole solution:
type JTDDataDef<S, D extends Record<string, unknown>> =
| (// ref
S extends { ref: string }
? JTDDataDef<D[S["ref"]], D>
: // type
S extends { type: "string" }
? string
: // properties
S extends {
properties: Record<string, unknown>
}
? { -readonly [K in keyof S["properties"]]-?: JTDDataDef<S["properties"][K], D> }
:
never)
| (S extends { nullable: true } ? null : never)
type JTDDataType<S> = S extends { definitions: Record<string, unknown> }
? JTDDataDef<S, S["definitions"]>
: JTDDataDef<S, Record<string, never>>
interface ValidateFunction<T> {
(data: unknown): data is T
}
type Overloading =
& (<S>(schema: JTDDataType<S>) => ValidateFunction<JTDDataType<S>>)
& (<T>(schema: T) => ValidateFunction<T>)
const compile1: Overloading = <T,>(schema: T): ValidateFunction<T> => {
return (() => true) as unknown as ValidateFunction<T>;
}
const llSchema = {
definitions: {
node: {
properties: {
val: { type: "string" },
next: {
ref: "node",
nullable: true
}
}
}
},
ref: "node",
nullable: true,
} as const;
const isLinkedList = compile1(llSchema);
function compile2<S>(schema: S): ValidateFunction<JTDDataType<S>> {
return (() => true) as unknown as ValidateFunction<JTDDataType<S>>;
}
type Overloading2 =
& (<S>(schema: S) => (data: unknown) => data is JTDDataType<S>)
& (<T>(schema: unknown) => (data: unknown) => data is T)
const compile3: Overloading2 = <T,>(schema: unknown): (data: unknown) => data is T => {
return (() => true) as unknown as (data: unknown) => data is T;
}
But honestly, I don't think function types definitions are good.
It is a bad practice to define only one overloading for function. It is unsafe.
You use a lot double type casting. This is no good.
Let's back to your main question:
Why it throws an error ?
TypeScript has his own recursive limitation. So, here ValidateFunction<JTDDataType<S>>
TS is unable to fidure out how deep the recursion will be. It just no way.
Here (my article), here (github issue) and here (my article) you can find more information about
This isn't an "implicit casting to any"; it's a recursion guard to prevent literally infinite computation. If S needs to compare itself to S<S> to determine assignability, and then S<S> needs to compare itself to S<S<S>>, then S<S<S>> needs to compare itself to S<S<S<S>>>, then S<S<S<S>>> needs to compare itself to S<S<S<S<S>>>>, at that point the checking stops and it's assumed that the answer is "yes". This case actually does come up in practice quite a bit; if your model is dependent on this checking going arbitrarily deep, then effectively you're asking for the compiler to sometimes freeze forever checking S<S<S<S<S<S<S<S<S<S<S<S<S<S<S<S<S<S<S<S<S>>>>>>>>>>>>>>>>>>>>>>>> against S<S<S<S<S<S<S<S<S<S<S<S<S<S<S<S<S<S<S<S<S<S>>>>>>>>>>>>>>>>>>>>>>>>> (which in turn will check S<SS<S<S<S<S<S<S<S<S<S<S<S<S<S<S<S<S<S<S<S<S>>>>>>>>>>>>>>>>>>>>>>>>>>.
Upvotes: 1