Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to type function return type with optional object of formatter function

I'm trying to figure out how to properly type the return value when an object with optional formatter functions is involved.

For a function with a single value it is working fine.

type Params = {
    id?: number
    created?: string
}

type FormatFn<TParam extends keyof Params, TValue> = (data: Params[TParam]) => TValue

type Result<TValue> = {
    param: TValue
}

declare function query<TParam extends keyof Params, TValue = Params[TParam]>(
    param: TParam,
    formatter?: FormatFn<TParam, TValue>
): Result<TValue>;

// Works as expected 'created' is type 'Date'
const { param: created } = query('created', (created) => new Date(created || ''))

I want to add a version where you can input an array of strings and an object of optional formatter functions.

Here is a Playground with my attempt.

This would be my expected output:

queries(['id', 'created'], {
    created: (created) => new Date(created || '')
})

// Instead of return type
// {
//     params: {
//         id: number,
//         created: string
//     }
// }
// 
// I want to have this, because of the formatter function
// {
//     params: {
//         id: number,
//         created: Date
//     }
// }
like image 366
fnn Avatar asked Mar 05 '26 02:03

fnn


1 Answers

type Params = {
    id?: number
    created?: string
}

type Elem = keyof Params;

type Fn = (value: any) => any

type Predicate<T extends Elem> = Record<T, (value: Required<Params>[T]) => any>

type Reducer<
    Arr extends ReadonlyArray<Elem>,
    Result extends Record<string, any> = {}
    > = Arr extends []
    ? Result
    : Arr extends readonly [infer H, ...infer Tail]
    ? Tail extends ReadonlyArray<Elem>
    ? H extends Elem
    ? Reducer<Tail, Result & Predicate<H>>
    : never
    : never
    : never;

/**
 * Pure js/ts analogy
 */

const reducerPredicate = (elem: string) => ({ [elem]: () => null });

const reducer = <Arr extends string[]>(
    arr: Arr,
    result: { [prop: string]: string } = {}
) => {
    if (arr.length === 0) {
        return result; // 1. end of recursion, this is the last call
    }

    const [head, ...tail] = arr; // 2. from first and before the last

    return reducer(tail, { ...result, ...reducerPredicate(head) });
    // no need for never branch
};

type Result<
    P extends ReadonlyArray<keyof Params>,
    Defaults extends Partial<Record<keyof Params, Fn>>,
    Cache extends P[number] & keyof Defaults = P[number] & keyof Defaults> =
    & { [Prop in Exclude<P[number], Cache>]: Required<Params>[Prop] }
    & { [Prop in keyof Defaults]: Defaults[Prop] extends Fn ? ReturnType<Defaults[Prop]> : never }

function queries<TParams extends Array<keyof Params>, TValues extends Partial<Reducer<TParams>>>(
    params: [...TParams],
    formatter?: TValues
): Result<TParams, TValues> {
    return null as any
}

const multiple = queries(['id', 'created'], {
    created: (created /** string */) => 42,
    id: (value /** number */) => 42
})

Playground

Here, in my blog, you can find more explanation.

like image 66
captain-yossarian Avatar answered Mar 07 '26 16:03

captain-yossarian



Donate For Us

If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!