Erik
Erik

Reputation: 7342

Getting error 2589 only in overload signature

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.

[playground]

Upvotes: 1

Views: 191

Answers (1)

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;
}

Playground

But honestly, I don't think function types definitions are good.

  1. It is a bad practice to define only one overloading for function. It is unsafe.

  2. 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

Related Questions