Aaron Aaron - 1 month ago 6
TypeScript Question

TypeScript function implementation downgrades parameter types to `any` if you declare a param type

I have a function interface like this:

interface Callback {
(a: string, b: number): void;
}


And I can implement it without declaring parameter types like this:

const callback: Callback = (a, b) => { }


In this case TypeScript understands the parameter types of
callback
are actually
(a: string, b: number)
.

However, if I declare it with one parameter typed, such as
b: number
:

const callback: Callback = (a, b: number) => { }


The other parameter
a
's type becomes
any
. Example in the Playground. What's odd is that the compiler does know what type
a
should be, because it won't let you define it wrongly, for example
(a: boolean, b: number)
will say the parameters are incompatible. Why won't it infer the parameter type of
a
?




The above is a trivial example, but it's giving me some headache when trying to generate a type-safe Redux reducer map:

interface IReducer<TState> {
(state: TState, action: IAction): TState;
}

interface IReducerMap<TState> {
[actionType: string]: IReducer<TState>;
}

interface MyState { hello: string; }

interface MyAction extends IAction { say: string; }

const myReducerMap: IReducerMap<MyState> = {
// Result: `(state: MyState, action: IAction) => MyState`
// But I get an error on `action.say` not defined in `IAction`
reducer1: (state, action) => {
return { hello: action.say };
},

// Result: `(state: any, action: MyAction) => computed`
reducer2: (state, action: MyAction) => {
return { hello: action.say + state.this_should_be_an_error };
},

// Works but relies on you to correctly defining state
reducer3: (state: MyState, action: MyAction) => {
return { hello: action.say };
}
}


Since each function will take a subtype of
IAction
as its
action
parameter (in this case
MyAction
), I must declare its type in the callback parameter. But once I declare its type, I lost the type of
state
and I must declare it. When I have dozens of callbacks and a real state name like
DataImportMappingState
for every callback, this is quite annoying.

Answer

The inference of a and b comes from contextual typing. Previously, parameters were only contextually typed if all parameters were unannotated.

This behavior has changed in the latest build of the TypeScript compiler because it seemed to be unintuitive. Now contextual typing will apply to all parameters which don't have explicit type annotations.