See microsoft/TypeScript#51488 for a canonical answer to this question.
Let's define
type F<T> = T extends undefined ? () => void : (params: T) => void
so that your function is of type
function useExample<
TParams extends object | undefined = undefined
>() {
const open: F<TParams> = (...arg: [TParams] | []) => { };
return open;
}
and we can just refer to F<T>
for brevity in what follows.
The problem you're having is that the compiler doesn't see (...arg: [TParams] | []) => void
as assignable to F<TParams>
, even though from the definition of F<T>
it's assignable to both sides of the conditional.
When you have a conditional type that depends on a generic type parameter, like F<T>
, the compiler mostly defers evaluation of it and the type is therefore essentially opaque. The compiler doesn't really try to see what values might or might not be assignable to such a type, at least until the type parameter is actually specified with a type argument. This is especially true when the conditional type is distributive (where the type being checked is a bare type parameter, such as T extends ⋯ ? ⋯ : ⋯
as opposed to SomethingElse<T> extends ⋯ ? ⋯ : ⋯
).
Distributive conditional types distribute across unions, so if G<T>
is a distributive conditional type, G<A | B>
will evaluate to G<A> | G<B>
. Note that the never
type is considered the empty union, and therefore G<never>
will be never
no matter what (this makes sense if you realize that A | never
is A
, so if G<A | never>
distributes to G<A> | G<never>
, then G<A>
is always G<A> | G<never>
for all possible A
and distributive G
, so G<never>
had probably better be never
). Since your type F<T>
is distributive, it means that F<never>
is never
... not () => void
or (params: never) => void
. Since open
is not a valid F<never>
, it is technically correct for the compiler to complain about the assignment. Even if it weren't, it's too complicated to figure out if a value will be assignable to F<T>
for all possible union-typed T
, and the compiler doesn't even try. So you get an error.
What can be done? In general when you have a generic conditional type and you need to assign a value to it, you'll probably have to just use a type assertion and move on:
function useExample<
TParams extends object | undefined = undefined
>() {
const open = ((...arg: [TParams] | []) => { }) as F<TParams>;
return open;
}
But in this particular case, you probably don't actually want F<T>
to be distributive in T
(right? You don't want F<string | number>
to be ((params: string) => void) | ((params: number) => void)
, do you?) so you can follow the directions in the documentation about distributive conditional types: "To avoid that behavior, you can surround each side of the extends keyword with square brackets." (Also see How to avoid distributive conditional types for more information) Like this:
type F<T> = [T] extends [undefined] ? () => void : (params: T) => void;
And suddenly everything works:
function useExample<
TParams extends object | undefined = undefined
>() {
const open: F<TParams> = (...arg: [TParams] | []) => { }; // okay
return open;
}
That's because for non-distributive conditional types, the compiler does check if the value is assignable to each side and allows the assignment if so. The type F<TParams>
will definitely either be () => void
or (params: TParams) => void
, and since (...arg: [TParams] | []) => void
is assignable to both of those, the assignment is accepted.
So that's probably what you want to do in this particular case, even though the general version of this issue (where you might need distributive conditional types) is not solvable without something like type assertions.
Playground link to code