func3
requires passing either Interface1
or Interface2
when calling func1
.
The type signature of func1
is:
(state: Interface1) => void
If you intend to pass a callback, the argument type of func3
needs adjustment:
type Interface1 = {
tag: 'Interface1'
}
type Interface2 = {
tag: 'Interface2'
}
const func1 = (state: Interface1) => {
//some code
}
const func2 = (state: Interface2) => {
//some other code
}
const func3 = (cb: typeof func1 | typeof func2) => {
//some other code
}
func3(func1) // okay
If you're interested in function composition, check out this article.
UPDATE:
Previous solutions work; however, calling callback
is impossible. My mistake, apologies for that.
To enable this functionality, some code refactoring is required as union types alone may not suffice with functions.
Consider the following example:
type Union = typeof func1 | typeof func2
const func3 = (cb: Union) => {
cb({ tag: 'Interface2' }) // error
}
func3(func1) // okay
In the above scenario, cb
is inferred as (arg:never)=>any
. Why?
Please refer to @jcalz's excellent answer on intersections.
The crux here is - types in contravariant positions get intersected.
Due to the impossibility of creating Interface1 & Interface2
, it resolves to never
.
For more insights on contravariance
, variance
, etc., please visit my question.
Hence, TS struggles to differentiate allowed vs. disallowed arguments.
As witnessed in the example, although func1
was used as an argument, attempting to call the callback with Interface2
could lead to runtime errors.
A workaround can be implemented as follows:
type Interface1 = {
tag: 'Interface1'
}
type Interface2 = {
tag: 'Interface2'
}
const func1 = (state: Interface1) => {
//some code
}
const func2 = (state: Interface2) => {
//some other code
}
type Fn = (...args: any[]) => any
function func3<Cb extends Fn, Param extends Parameters<Fn>>(cb: Cb, ...args: Param) {
cb(args)
}
const x = func3(func1, { tag: 'Interface1' }) // okay
Function arguments are contravariant to each other if attempting to create a union of functions leading to errors.
UPDATE 3:
If the desired outcome differs from expectations, utilizing a typeguard within func3
to compute func1
argument is necessary:
type Interface1 = {
tag: 'Interface1'
}
type Interface2 = {
tag: 'Interface2'
}
const func1 = (state: Interface1) => {
//some code
}
const func2 = (state: Interface2) => {
//some other code
}
type Fn = (a: any) => any
// typeguard
const isFunc = <R extends typeof func1 | typeof func2>(cb: Fn, cb2: R): cb is R => cb === cb2
const func3 = (cb: typeof func1 | typeof func2) => {
if (isFunc(cb, func1)) {
cb({ tag: 'Interface1' })
} else {
cb({ tag: 'Interface2' })
}
}
func3(func1) // okay