Search code examples
typescriptconditional-typeskeyoffunction-signature

How to distinguish different functions signature with conditional type checks?


I'd like to be distinguish the following function types in conditional type checks:

type SyncFn = () => void;
type AsyncFn = (data: number) => Promise<void>;
type SyncFnWithArg = (data: number) => void;

So I can then use the KeyOfType that @Titian Cernicova-Dragomir posted and get the keys within a given interface that match a given type.

I tried the following:

type SyncFn = () => void;
type AsyncFn = (data: number) => Promise<void>;
type SyncFnWithArg = (data: number) => void;

interface Foo {
    a?: string;
    b?: number;
    c: number;
    d: string;
    f1?: SyncFn;
    f2?: AsyncFn;
    f3?: SyncFnWithArg;
}

// note: `KeyOfType` from https://stackoverflow.com/questions/49752151/typescript-keyof-returning-specific-type
type KeyOfType<T, V> = keyof { [P in keyof T as T[P] extends V? P: never]: any }

type KeyOfTypeOptionalIncluded<T, Condition> = KeyOfType<T, Condition | undefined>


let onlyStrings: KeyOfTypeOptionalIncluded<Foo, string>;
onlyStrings = 'a' // ✅ working as expected 🎉
onlyStrings = 'b' // ✅ erroring out as expected 🎉
onlyStrings = 'd' // ✅ working as expected 🎉


let onlySyncFn: KeyOfTypeOptionalIncluded<Foo, SyncFn>;
onlySyncFn = 'f1' // ✅ working as expected 🎉
onlySyncFn = 'f2' // ✅ erroring out as expected 🎉
onlySyncFn = 'f3' // ✅ erroring out as expected 🎉

let onlyAsyncFn: KeyOfTypeOptionalIncluded<Foo, AsyncFn>;
onlyAsyncFn = 'f1' // ✅ erroring out as expected 🎉
onlyAsyncFn = 'f2' // ✅ working as expected 🎉
onlyAsyncFn = 'f3' // ✅ erroring out as expected 🎉

let onlySyncFnWithArg: KeyOfTypeOptionalIncluded<Foo, SyncFnWithArg>;
onlySyncFnWithArg = 'f1' // 😭 should error out 😭
onlySyncFnWithArg = 'f2' // 😭 should error out 😭
onlySyncFnWithArg = 'f3' // ✅ working as expected 🎉

TS Playground

The problem is that onlySyncFnWithArg is being typed as "f1" | "f2" | "f3" whereas it should be "f3"....

enter image description here


I also noticed that if I modify AsyncFn and remove its argument then I have more problems since the type definition for onlySyncFn is now incorrect since now it's "f1" | "f2" instead of only being "f1" as it is in the first TS Playground above.

Second TS Playground

I guess that's related with how function overloading in typescript is done, but I don't really know, so that's why I'm reaching out for help.... maybe it's not related, but are we able to do such function type distinction in TS?


Solution

  • The problem can be addressed by changing the KeyOfType type as follows:

    1. Check both directions of the type relationship (A extends B and B extends A):
    2. Wrap the types used in the conditional type clause in tuples ([A] extends [B]).
    type KeyOfType<T, V> = keyof {
      [P in keyof T as [T[P]] extends [V]
        ? [V] extends [T[P]]
          ? P
          : never
        : never
      ]: any
    }
    

    Find a playground example here, and an interesting discussion here about various ways to test for type equality (each with their own caveats).