Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Why can't Typescript infer function argument types for optional arguments?

I think it's clearer to try it on TS Playground:

function identity<T extends (...args: any[]) => any>(fn: T): T {
  return fn;
}

function fn(args: {
  cb: (foo: number) => void,
}) {}

fn({
  cb: identity((foo /* infers number */) => {}),
});

function fn2(args: {
  cb?: (foo: number) => void,
}) {}

fn2({
  cb: identity((foo /* doesn't infer number */) => {}),
});

function fn3(args: {
  cb: (foo: number) => void,
} | {}) {}

fn3({
  cb: identity((foo /* infers number */) => {}),
});

For fn and fn3, TS was able to infer that foo is a number. However, for fn2, TS just typed foo as any. The typing for fn2 and fn3 are functionally the same thing, so I'm wondering why TS couldn't infer the type of foo.

The realistic use-case for this is React's useCallback, I was trying to infer the argument types for functions that pass through useCallback.

Why does TS behave like this? Is there a less hacky solution?

like image 450
Leo Jiang Avatar asked Apr 03 '21 08:04

Leo Jiang


People also ask

How do I type an optional argument in TypeScript?

In Typescript, making optional parameters is done by appending the “?” at the end of the parameter name in the function when declaring the parameters and the parameters which are not marked with “?” i.e not optional parameter are called as default parameters or normal parameters where it is must and compulsory to pass ...

How does TypeScript support optional parameters in function?

TypeScript provides a Optional parameters feature. By using Optional parameters featuers, we can declare some paramters in the function optional, so that client need not required to pass value to optional parameters.

How are optional parameters implement TypeScript?

Use the parameter?: type syntax to make a parameter optional. Use the expression typeof(parameter) !== 'undefined' to check if the parameter has been initialized.

How does TypeScript type inference work?

TypeScript infers types of variables when there is no explicit information available in the form of type annotations. Types are inferred by TypeScript compiler when: Variables are initialized. Default values are set for parameters.


1 Answers

I think the crucial step here is the inference of identitys return type:

 let result: ((foo: number) => void | undefined) = identity(...);

As the return type has a base constraint of (...args: any[]) => any this rule of the Typescript specification applies:

The inferred type argument for each type parameter is the union type of the set of inferences made for that type parameter. However, if the union type does not satisfy the constraint of the type parameter, the inferred type argument is instead the constraint.

~ TypeScript Language Specification (outdated), Contextual Signature Instantiation

As undefined (the optional value) does not satisfy the constraint, the constraint is taken, and T gets inferred as (...args: any[]) => any.

By removing the constraint, T gets inferred correctly

like image 128
Jonas Wilms Avatar answered Oct 20 '22 10:10

Jonas Wilms