Search code examples
typescriptgenericstuplesconditional-types

Inferring function signature from tuple in Typescript


I tried for long hours now and honestly it wasn't worth the effort, bu I'd still like to see if there's a solution to this:

I'm trying to force TS to infer my function signature from tuples. I tried to play with conditional types, but I don't seem to get it right:

// model
interface User {
    name: string
}

interface Article {
    title: string
}

/// api
type Resource = 'user' | 'articles'
type Params = Record<string, number | string | boolean>

type GetSignature =
    | ['user', undefined, User]
    | ['articles', { articleId: number }, Article[]]
    | ['articles', undefined, Article]

// get request
interface ObjArg {
    resource: Resource
    params?: Params
}

type RetType<TResult> = TResult & { cancel: () => void }

async function get<TResult>(args: ObjArg): Promise<RetType<TResult>>
async function get<TResult>(resource: Resource, params?: Params): Promise<RetType<TResult>>
async function get<TResult>(args: [ObjArg | Resource, Params?]): Promise<RetType<TResult>>{
    const { resource, params } = typeof args[0] === 'object' ? args[0] : { resource: args[0], params: args[1] } 
    const result = await someAsyncFetch(resource, params)
    return { ...result, cancel: () => { cancelAsyncFetch() }}
}

I'd like TS to be able to infer the get's signature from provided arguments, so it automatically knows that e.g. when calling get('articles', { articleId: 1 }) the return type should beArticleas well as that I need the second argument to be of type{articleId: number}(orundefinedfor array of articles). This is whatGetSignature` union type should define.

So the desired usage would be something like

const user = get('user') // returns User
const article = get('articles', { articleId: 1 }) // returns Article
const articles = get('articles') // returns Article[]

I tried dozens of approaches and none seemed to provide the interface I aim for. Just to mention one of them, I tried to expect signature as a type argument (get<TSignature exntends GetSignature>(...)) and tried to infer the desired signature like in likes of this:

resource: TSignature[0] extends infer T ? T : never

or even

resource: TSignature[0] extends infer T ? Extract<GetSignature, T> : never

But nothing seemed to work. For now I think I'll stick to providing the type argument for TResult, but I'd like to know whether there a way to do what I describe in TS?


Solution

  • It looks like you want GetSignature to be this:

    type GetSignature =
        | ['user', undefined, User]
        | ['articles', { articleId: number }, Article]
        | ['articles', undefined, Article[]]
    

    (note how I swapped Article with Article[] to match with what you are expecting at the end).


    Given that, I'd probably separate out the one-arg and two-arg signatures using the Extract and Exclude utility types:

    type OneArgSignatures = Extract<GetSignature, [any, undefined, any]>;
    type TwoArgSignatures = Exclude<GetSignature, [any, undefined, any]>;
    

    And then define the call signature for get() to be a pair of overloads, one for one-arg and one for two-arg calls:

    declare function get<R extends OneArgSignatures[0]>(
        resource: R
    ): Extract<OneArgSignatures, [R, any, any]>[2];
    
    declare function get<R extends TwoArgSignatures[0]>(
        resource: R,
        params: Extract<TwoArgSignatures, [R, any, any]>[1]
    ): Extract<TwoArgSignatures, [R, any, any]>[2];
    

    You can see that the functions are generic in R, the resource string literal type. Note that the compiler generally has the easiest time inferring a type parameter from a value of that type. Inferring TSignature from resource: TSignature[0] extends infer R ? R : never is problematic, but inferring R from resource: R is straightforward. Once R is inferred, the compiler can use Extract to calculate the type of params (if present) and the return type.


    Let's see if it works:

    const u = get("user"); // User
    const aa = get("articles"); // Article[]
    const a = get("articles", { articleId: 123 }); // Article
    

    Looks good. Note that I didn't worry about Promises, RetType, or your other overload. I assume you can take care of that. Okay, hope that helps; good luck!

    Playground link to code