I'm trying to find a way to get a strict parameters discrimination from a function overload. Obviously my issue with this implementation is that my generic type T
can be extended to anything inheriting AorB
props so the error I get here is perfectly expected ('{ type: "A"; a: any; }' is assignable to the constraint of type 'T', but 'T' could be instantiated with a different subtype of constraint 'AorB'.
).
What I'm looking for is a way to have a sort of <T implements AorB>
so that when the parameter type
equals "A"
, the customProps
parameter is discriminated as A
props.
I'm also trying to avoid resorting to use any
within the function implementation parameters or as
within the returns.
type A = {
a: string
type: 'A'
}
type B = {
b: string
type: 'B'
}
type AorB = A | B
function createAorB(type: A['type'], customProps?: Partial<Omit<A, 'type'>>): A
function createAorB(type: B['type'], customProps?: Partial<Omit<B, 'type'>>): B
function createAorB<T extends AorB>(type: T['type'], customProps: Partial<Omit<T, 'type'>> = {}): T {
if (type === 'A') {
return {
type,
a: customProps.a || '',
}
}
return {
type,
b: customProps.b || '',
}
}
const newA = createAorB('A')
const newB = createAorB('B')
UPDATE
If I enforce AorB
as the return value:
function createAorB<T extends AorB>(type: T['type'], customProps: Partial<Omit<T, 'type'>> = {}): AorB
I get both errors Property 'a' does not exist on type 'Partial<Omit<T, "type">>'.
and Property 'b' does not exist on type 'Partial<Omit<T, "type">>'.
on the respective customProps.a
and customProps.b
lines.
CodePudding user response:
The argument list tuple type of createAOrB()
forms a discriminated union, where the first tuple element is the discriminant. It looks like this:
type Args =
[type: "A", customProps?: Partial<Omit<A, "type">>] |
[type: "B", customProps?: Partial<Omit<B, "type">>];
If you use a rest parameter and destructuring assignment to assign the elements to variables named type
and customProps
, you can annotate the rest parameter as Args
, and then the compiler will use control flow analysis to narrow customProps
when you check type
:
function createAorB(type: A['type'], customProps?: Partial<Omit<A, 'type'>>): A
function createAorB(type: B['type'], customProps?: Partial<Omit<B, 'type'>>): B
function createAorB(...[type, customProps]: Args) {
if (type === 'A') {
return {
type,
a: customProps?.a || '', // okay
}
}
return {
type,
b: customProps?.b || '', // okay
}
}
If you happen to have lots of types in AorB
and not just A | B
, it might be tedious to extend the Args
definition. In this case you can have the compiler compute it for you as a function of AorB
, like this:
type Args = AorB extends infer T ? T extends AorB ? (
[type: T['type'], customProps?: Partial<Omit<T, 'type'>>]
) : never : never;
This uses conditional type inference to copy AorB
into a new generic type parameter T
, which is then used to make a distributive conditional type so that [type: T['type'], customProps?: Partial<Omit<T, 'type'>>]
automatically becomes a union type if T
is a union type (it distributes the operation over unions in T
).
You can verify that it evaluates to the same as the manually defined version above.