Consider the following JS function and example usage:
const fooInteractor = function(state, foo) {
if (foo) {
return {...state, foo}
} else {
return state.foo
}
}
const state = {foo: 'foo', other: 'data'};
fooInteractor(state); // returns 'foo'
fooInteractor(state, 'bar'); // return {foo: 'bar', other: 'data'}
// we pass this function around generically and use it to interact with state
function interacts(state, interactorFn) {
// ie interactorFn(state);
}
interacts(state, fooInteractor);
How can I type this in TS land?
I can define a type and interaction with that type that Typescript is happy with:
type StateInteractor {
(state: State): string
(state: State, value: string): State
}
function interact(state: State, stateFn: StateInteractor) {
const data: string = stateFn(state);
const changedState: State = stateFn(state, data);
// etc
}
However, when it actually comes to implementing StateInteractor
I'm having issues:
const fooInteractor: StateInteractor = function(state: State, foo?: string) {
if (foo) {
return {...state, foo} as State
} else {
return state.foo as string
}
}
/*
Type '(state: State, foo?: string) => string | State' is not assignable to type 'StateInteractor'.
Type 'string | State' is not assignable to type 'string'.
Type 'State' is not assignable to type 'string'.ts(2322)
*/
I also can't get arrow functions / lambda to work.
Note that I can do this with function overloading:
function fooInteractor(state: State): string
function fooInteractor(state: State, value: string): State
function fooInteractor(state: State, value?: string) {
if (value) {
return {...state, foo: value} as State
} else {
return state.foo as string
}
}
// This compiles and works as expected
const typedFooInteractor: StateInteractor = fooInteractor
interact(someState, typedFooInteractor)
But this is super ugly and weird, and requires that I redefine the StateInteractor type (via overloading) every single time I implement one.
CodePudding user response:
We can squeeze the overload logic into a generic function signature using Generic Parameter Defaults and a Conditional Type as the return value. Playground version
V extends string | undefined = undefined
(the generic default) says V
is either string or undefined and by default undefined. The conditional return V extends string ? State : string
uses ternary syntax to return based on V
's type.
type State = Record<string, any>
// define a function type whose return value depends on an argument
type StateInteractor = <S extends State, V extends string | undefined = undefined>(state: S, value?: V) => V extends string ? State : string
// define an instance
const fooInteractor: StateInteractor = (state: State, foo?: string) => {
if (foo) {
return {...state, foo}
} else {
return state.foo
}
}
// test it
const state = {foo: 'foo', other: 'data'};
const shouldBeFoo = fooInteractor(state); // returns 'foo'
const shouldBeState = fooInteractor(state, 'bar'); // return {foo: 'bar', other: 'data'}