Home > Mobile >  TypeScript: type for partial function arguments
TypeScript: type for partial function arguments

Time:10-04

I'm looking for some kind of "subset of function arguments"-type PartialFunction<fn>.

The function run accepts a single argument, which is a function that is guaranteed to be called with 2 arguments (number and string).

Now I wanted to create a type for a function that accepts the same order of arguments, but make them optional:

type MyFunction = (a: number, b: string) => string | undefined
type OtherFunction = PartialFunction<MyFunction>
// which then can be one of:
//     (a: number, b: string) => string | undefined
//     (a: number) => string | undefined
//     () => string | undefined

But maybe my problem can be solved a different way. So here's the code:

type MyFunction = (a: number, b: string) => string | undefined;

const run = (execFn: MyFunction) => {
    const result = execFn(42, "abc");
}

// here MyFunction can't be used for typing, because function argument b is missing
const log = (a: number): undefined => {
    console.log(a);
    return undefined;
}

// here MyFunction is valid, because a and b are present
const compare = (v?: number): MyFunction => (a, b) => {
    log(a);
    return v === a ? b : "";
}

run(log)
run(compare(42))

Basically I want to express that const log is "compatible" with MyFunction, using something like this:

const log: CompatibleWith<MyFunction> = (a) => {
    console.log(a);
    return undefined;
}

Is this somehow possible? Or is there some better solution to my problem?

CodePudding user response:

Let's first create a type to get our parameters:

type AllLess<T extends ReadonlyArray<unknown>> =
    T extends readonly [...infer Head, any]
        ? AllLess<Head> | T
        : T;

This type (with the lack of a better name) simply gives us a union of all the tuples "under" this one:

type T = AllLess<[1, 2, 3]>; // [1, 2, 3] | [1, 2] | [1] | []

Then your PartialFunction type could be implemented as:

type PartialFunction<F extends (...args: any[]) => any> =
    AllLess<Parameters<F>> extends infer P extends ReadonlyArray<unknown>
        ? P extends P ? (...args: P) => ReturnType<F> : never
        : never;

Distributing over the parameters we got from the previous type, we then get our union of functions, as desired. It is important to note that attempting to call a function of this type acts as if you are calling an intersection of functions instead. Meaning:

const fn: ((a: number) => void) | ((a: string) => void) = ...

// `fn` has type `(a: number & string) => void`...
// 'number & string' is 'never'...
fn(""); // ERROR: Type 'string' is not assignable to type 'never'

Fortunately, in the next version of TypeScript (4.9), we'll get the much wanted satisfies operator. It's the kind of thing that you don't know you want until you want it :)

const log = ((a: number) => {
    console.log(a);
    return undefined;
}) satisfies PartialFunction<MyFunction>;

Now, it isn't explicitly typed as PartialFunction<MyFunction>, but we can sleep easy knowing that log is now being checked if it is assignable to PartialFunction<MyFunction>.

Playground

CodePudding user response:

You could use a call signature to make your function accept an object and use the keys as different arguments. Then you can use the built-in Partial type to construct a type where each argument is optional.

I think the most readable way to achieve this is to first define a generic function, which can take any type as properties. For your original function, you would define it with a type GenericFunction<Props>, while for your "partial function" you would use GenericFunction<Partial<Props>>, like this:

type GenericFunction<PropsType> = (props: PropsType) => string | undefined;

type Props = {
  a: number;
  b: string;
};
type MyFunction = GenericFunction<Props>;
type OtherFunction = GenericFunction<Partial<Props>>;

Now you can use your definition to type the functions used in your example. You will just need to rewrite the call signature to accept an object.

const run = (execFn: OtherFunction) => {
    const result = execFn({a: 42, b: "abc"});
}

const log: OtherFunction = ({ a }) => {
    console.log(a);
    return undefined;
}

const compare = (v?: number): OtherFunction => ({a, b}) => {
    log({ a });
    return v === a ? b : "";
}

Check out the working example in the Typescript playground.

  • Related