Search code examples
typescripttypescript-types

Partial application of TypeScript object


I want to partially apply an object to a function that accepts a single object as argument. I have been able to make the partial function below return a function that behaves as expected, but I am not able to make the partial function reject extra keys for the provided x:

/******
 * Things that I couldn't make work.
 */
type Impossible<K extends keyof any> = {
  [P in K]: never;
};

type NoExtraProperties<T, U extends T = T> = U & Impossible<Exclude<keyof U, keyof T>>;

type Exactly<T, X> = T & Record<Exclude<keyof X, keyof T>, never>


/*********
 * Attempted implementation
 */
const partial = <T, X extends Partial<T> = Partial<T>>(fn: (arg: T) => unknown, x: X): ((arg: Omit<T, keyof X>) => ReturnType<typeof fn>) => {
    return (arg) => fn(Object.assign(x, arg) as T);
}

// Example function to partially apply
const fn = ({name, age, alive}: {name: string, age: number, alive: boolean}): string => {
    return name + age.toString()
}

// plain function call
const fullCallResult = fn({
    name: "Maono",
    //sadfadsfdas: "sdfadsfa", // Error here, as expected.
    age: 19,
    alive: true
});

const partialllyApplied = partial(fn, {
    name: "asdfasdf",
    sadfadsfdas: "sdfadsfa" // <- No error here. I want this to give an error.
    //, age: 32
    //, alive: true
});

const partialllyAppliedResult = partialllyApplied({agde: 44, alive: false}) // <-- Error here, good, there is a type.

const partialllyAppliedResult2 = partialllyApplied({age: 44, alive: false})

How could you make the partial function restrict what keys are accepted from the passed x object mimicking a direct function call?


Solution

  • function partialApply<T, R, X extends Partial<T>>(
        fn: (arg: T) => R,
        // X, but keyof T is not optional and unwanteds are never
        def: X & { [K in keyof X]-?: K extends keyof T ? T[K] : never }
        //      T, but keyof X is optional
    ): (rest: { [K in keyof (Omit<T, keyof X> & Partial<T>)]: T[K] }) => R {
        return function apply(rest) {
            // create new object, rest may override def
            return fn({ ...def, ...rest })
        }
    }
    
    // Example function to partially apply
    const fn = ({ name, age, alive }: { name: string, age: number, alive: boolean }): string => {
        return name + age.toString();
    }
    const apply1 = partialApply(fn, { age: 123 })
    //    ^?
    // const apply1: (rest: {
    //     name: string;
    //     alive: boolean;
    //     age?: number | undefined;
    // }) => string
    
    partialApply(fn, { age: 123 as 123 | undefined })
    //                 ~~~ Type '123 | undefined' is not assignable to type '123'.
    partialApply(fn, { age: 123 })({ alive: true })
    //                             ~~~~~~~~~~~~~~~ Property 'name' is missing in type '{ alive: true; }'