Please do not misuse TypeScript overloaded function types

TypeScript allows you to define overloaded function types, which are implemented by multiple consecutive overloaded declarations + one function implementation. such as

function func(n: number): void;
function func(prefix: string, n: number): void;
function func(first: string | number, n?: number): void {
    if (typeof first === "string") {
        console.log(`${first}-${n}`);
    } else {
        console.log(`number-${first + 10}`);
    }
}

The func() function in the example has two overloads:

  • (number) => void
  • (string, number) => void

In its implementation part, the parameter and return value declaration should be compatible with all overloads, so the first parameter may be number or string, that is, first: string | number; The second parameter may be number or no, that is, n?: number.

The declaration of overloaded functions can be defined by the interface declaration of functions. The above overloaded function types can be defined as follows:

interface Func {
    (n: number): void;
    (prefix: string,  n: number): void;
}

Check:

const fn: Func = func;

The above is "order"!

Now, forget func(), we have two functions defined respectively, func1() and func2():

function func1(n: number): void {
    console.log(`number-${n + 10}`);
}

function func2(prefix: string, n: number): void {
    console.log(`${prefix}-${n}`);
}

And there is a render() function, which wants to render the output according to the passed in function:

function render(fn: Func): void {
    if (fn.length === 2) {
        fn("hello", 9527);
    } else {
        fn(9527);
    }
}

So far, everything has not been a problem. The next step is to test render()

render(func1);
render(func2);

The problem is that neither func1 nor func2 can correctly match the parameter type of render()!!!

There is a misunderstanding that many people understand the overloaded function type. They think that if function f conforms to an overloaded signature of overloaded function type Fn, it should be used as this overloaded type.

In fact, if a function wants to match the overloaded function type, it must also be an overloaded function (or a function compatible with all overloaded types). Take the above example. If func1 is passed in, the render() runtime can accurately enter the else branch and successfully call fn(9527); Passing in func2 can also accurately enter the if branch and successfully call fn("hello", 9527). But——

These things are done by JavaScript at run time. During static analysis, the TypeScript compiler found that the parameter fn of render() needs to be compatible with the calls of fn(string, number) and fn(number). Neither func1() nor func2() have all the conditions.

Therefore, in the above example, the parameter passed into render() can only be an overloaded function func, not func1 or func2.

What if you want to achieve the original purpose?

Assuming that the types Func1 and Func2 are func1() and func2() respectively, the render() function should declare as follows:

function render(fn: Func1 | Func2) { ... }

However, in this way, the original function body of render() function will not work, because fn is one of the two types, but it is not sure which one is called. We need to write a type assertion function to help TypeScript inference. A complete example is as follows:

type Func1 = (n: number) => void;
type Func2 = (prefix: string, n: number) => void;

function isFunc2(fn: Func1 | Func2): fn is Func2 {
    return fn.length === 2;
}

function render(fn: Func1 | Func2): void {
    if (isFunc2(fn)) {
        fn("hello", 9527);
    } else {
        fn(9527);
    }
}

render(func1);  // number-9527
render(func2);  // hello-9527

Finally, it is summarized & emphasized that the combination of overloaded function types and various function types participating in overloaded types is completely different. Please pay attention to the difference and do not misuse it.

Tags: TypeScript

Posted on Tue, 02 Nov 2021 19:47:47 -0400 by cjkeane