FIRST seсоnd
FIRST seсоnd

Reputation: 11

How to Make TypeScript Infer Object Fields in a Tuple Return Type Based on Conditions?

I'm working on a TypeScript function that returns different objects based on the input parameter. The return type is a tuple where the first two elements are a number and a string, respectively, and the third element is an object. The structure of the returned object changes depending on the input. Here is a simplified version of my function:

function a(n: number) {
    if (n == 2)
        return [0, "", { some: "data" }];
    if (n == 3)
        return [0, "", { next: "step" }];
    return [0, "", { other: "some" }];
}
function b() {
    return [1, "some", {bebe: "baba"}]
}

I want TypeScript to infer the type of the third element in the tuple ({ some: "data" }, { next: "step" }, or { other: "some" }) based on the value of n, so that when I work with the return value of a, TypeScript can provide autocompletion for the fields of the object based on the known return type.

For example:

const result = a(2);
console.log(result[2].some); // Should know that `some` is a valid field
const result1 = b()
console.log(result1[2].bebe)

However, I'm struggling to define the return type of the function a in a way that allows TypeScript to correctly infer the possible object structures without explicitly defining all possible return types beforehand.

Is there a way in TypeScript to dynamically infer the structure of an object in a tuple return type based on runtime conditions? I'm looking for a solution that ideally does not require me to explicitly enumerate all possible object shapes as part of the function's return type.

I've tried do this, but got error "Return type annotation circularly references itself."

function a(n: number): [string, Record<keyof (ReturnType<typeof a>[2]), any>] {
    if (n == 2) return ["", { some: "data" }]
    if (n == 3) return ["", { next: "step" }]
    return ["", { other: "some" }]
}

Update

I found a solution for my task, but now I have a question - how can I do this so as not to use the creation of a new variable at the time of declaring the function.

function hasFailedResult<D, K extends keyof any>(func: (args: any) => [number, string, Partial<Record<K, D>>]) {
    return func as (args: any) => { 0: number, 1: string, 2: Partial<Record<K, D>> };
}

const someFunc = hasFailedResult(function some(a: number) {
    if (a == 1)
        return [0, "", { next: "me" }]
    return [0, "", { some: "data" }]
})
someFunc(1)[2].
// -----------^next?: string
// -----------^some?: string

Upvotes: 1

Views: 96

Answers (1)

FIRST seсоnd
FIRST seсоnd

Reputation: 11

My main task was to decorate the returned data type with a function, explicitly specifying which fields will definitely be there, but at the same time I did not want to lose the types that TS itself generates. The solution I found is to create a decorator function that converts the type of function through generics.

function hasFailedResult<D, K extends keyof any>(func: (args: any) => [number, string, Partial<Record<K, D>>]) {
    return func as (args: any) => { 0: number, 1: string, 2: Partial<Record<K, D>> };
}

const someFunc = hasFailedResult(function some(a: number) {
    if (a == 1)
        return [0, "", { next: "me" }]
    return [0, "", { some: "data" }]
})
someFunc(1)[2].
// -----------^next?: string
// -----------^some?: string

OR

function hasFailedResult<
    K extends keyof any,
    T extends (args: any) => [number, string, D],
    D extends Record<K, any>
>(func: T) {
    return func as (args: any) => { 0: number, 1: string, 2: ReturnType<T>[2] };
}

const someFunc = hasFailedResult(function some(a: number) {
    if (a == 1)
        return [0, "", { next: "me" }]
    return [0, "", { some: "data", data: "me" }]
})

const d =  someFunc(1)[2];

/* it will be union type
 const d: {
    next: string;
} | {
    some: string;
    data: string;
}
*/

Upvotes: 0

Related Questions