Reputation: 173
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 be
Articleas well as that I need the second argument to be of type
{articleId: number}(or
undefinedfor array of articles). This is what
GetSignature` 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?
Upvotes: 1
Views: 359
Reputation: 327994
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 Promise
s, RetType
, or your other overload. I assume you can take care of that. Okay, hope that helps; good luck!
Upvotes: 1
Reputation: 2284
You can try this:
type GetSignature = {
user: [User, undefined, User]
articles: [Article, { articleId: number }, Article[]]
}
declare function get<K extends keyof GetSignature>(type: K): Promise<RetType<GetSignature[K][0]>>
declare function get<K extends keyof GetSignature>(type: K, param: GetSignature[K][1]): Promise<RetType<GetSignature[K][2]>>
async function get(type, param) {
...
}
Upvotes: 1