Search code examples
typescripttypescript-generics

Seeking feedback on simplifying TypeScript (I have a bad habit of overcomplicating things)


here's a pattern I use to calculate the output type of a series of functions applied to an object. Could you tell me if this approach is considered 'obscure' and should be avoided in favor of something simpler? A colleague didn't understand a code similar to this one and strongly advised me to simplify it:

type TFunction<P = any, R = any> = (arg: P) => R;

type UnionToIntersection<U> = (U extends unknown ? TFunction<U> : never) extends TFunction<infer I> ? I : never;

type ReturnPipeline<TPipeline extends TFunction[]> = 
    UnionToIntersection<ReturnType<TPipeline[number]>>;

type Pipeline = [
    TFunction<any, { a: true }>,
    TFunction<any, { b: true }>,
    TFunction<any, { c: true }>,
];

const pipeline : Pipeline  = [
    (arg:any) => ({ a: true}),
    (arg:any) => ({ b: true}),
    (arg:any) => ({ c: true}),
];

type MergedPipelineResult = ReturnPipeline<Pipeline>;

const result = pipeline.reduce<MergedPipelineResult>((acc, fct) => {
    return { ...acc, ...fct(acc) };
}, {} as any); // { a: true; b: true; c: true; }

My goal is to have a return type for this chain of functions because I need it later, but it seems to be not very readable. Maybe I should write the return type manually instead of generating it with the individual types of each function?

I'll try to simplify the code, but it's a habit of mine to create these "overly complicated" types.


Solution

  • If you want to keep exact return type, for example true instead of boolean you could using as const on every function and the function array, but seems not clean. So some factory function could be used. Also you could avoid many intermediate generic types and make only one type:

    Playground

    type TFunction<P = any, R = any> = (arg: P) => R;
    
    type ReturnPipeline<TPipeline extends readonly TFunction[]> = 
        TPipeline extends [] ? {} : 
        TPipeline extends readonly [infer A, ...infer B extends TFunction[]] ? A extends TFunction<any, infer C> ? {-readonly [K in keyof C]:C[K]} & ReturnPipeline<B> : never : never;
    
    const makePipeline = <const T extends object[]>(args: {[I in keyof T]: TFunction<any, T[I]>}) => args;
    
    const pipeline = makePipeline([
        (arg:any) => ({ a: true}),
        (arg:any) => ({ b: true}),
        (arg:any) => ({ c: true}),
    ]);
    
    const result = pipeline.reduce<ReturnPipeline<typeof pipeline>>((acc, fct) => {
        return { ...acc, ...fct(acc) };
    }, {} as any); // { a: true; b: true; c: true; }