Reputation: 379
These are the types I have:
type Action<T> = T extends undefined ? {
type: string;
} : {
type: string;
payload: T;
}
type ApiResponse<T> = {
ok: false;
error: string;
} | {
ok: true;
data: T;
};
I've defined this function:
function handleApiResponse<T>(apiResponse: ApiResponse<T>) {
const a: Action<ApiResponse<T>> = {
type: "response",
payload: apiResponse,
}
}
The problem is a
has an error because the conditional type in Action
is distributed over ApiResponse
.
What I need from Action<T>
is a type that is literally what it says, i.e., there are two cases:
Action<T>
is undefined, in which case Action<undefined>
= { type: string }
Action<T>
is anything else, in which case Action<T>
= { type: string, payload: T }
But this doesn't work when T
is a union type because of distributed conditional types.
How can I define a type like this that works even when T
is a union?
Upvotes: 6
Views: 1339
Reputation: 329598
As you mentioned, your definition of Action<T>
is a distributive conditional type, in which unions in T
are split up into their individual members (e.g., B | C | D
), passed through Action<T>
, and the result put back into a new union (e.g., Action<B> | Action<C> | Action<D>
). This is often the behavior people want from their conditional types, but it's not what you want here; that is, you've used a distributive conditional type accidentally.
A conditional type of the form TTT extends UUU ? VVV : WWW
will only be distributive if the type TTT
that you're checking is a bare generic type parameter like the T
in your definition of Action<T>
. If it's some specific type (like string
or Date
), it won't be distributive. And if it's a more complicated expression involving a type parameter (like {x: T}
), it won't be distributive. I sometimes refer to this latter idea, where you take the "bare" type parameter T
and do something to it like {x: T}
, as "clothing" the type parameter.
So you've got T extends undefined
, which is distributive:
type Action<T> = T extends undefined ? ... : ...
If you want to turn that off, you need to rephrase the check so that T
is clothed but where it has the same behavior (so the check succeeds if and only if T
extends undefined
). The easiest way to do this is to clothe both sides of the extends
clause to be a one-element tuple type), so T
becomes [T]
and undefined
becomes [undefined]
:
type Action<T> = [T] extends [undefined] ? ... : ...
and then your code works:
function handleApiResponse<T>(apiResponse: ApiResponse<T>) {
const a: Action<ApiResponse<T>> = {
type: "response",
payload: apiResponse,
} // okay
}
Note that TypeScript considers tuples and arrays to be covariant in their element types, meaning that Array<X> extends Array<Y>
or [X] extends [Y]
if and only if X extends Y
. This is technically unsafe to do (see this question and its answers for more info) but very convenient.
So that's a general rule you can use: if you have TTT extends UUU ? VVV : WWW
and it's accidentally distributive, you can turn that off by writing [TTT] extends [UUU] ? VVV : WWW
. This is mentioned at the bottom of the documentation for distributive conditional types, although it just sort of says "do this" and not why it works. It might look like the square brackets are some kind of special syntax designed for conditional types... but it's not. It's just using existing one-element tuple syntax, which happens to do what we want without requiring too many more characters.
Upvotes: 13