There is a type with unique property (method)
type Actions = {
method: "connections",
request: number,
response: number,
} | {
method: "delete",
request: string,
response: string,
}
I want to make a function which will accept (request) and process (response) values of type based on the unique field.
type Fn = <A extends Actions>(a: Pick<A, 'method' | 'request'>) => Pick<A, 'method' | 'response'>;
But when I call the implementation, e.g.:
const x = fn({ method: "connections", request: 10 })
x2.response
I get following type for response field
string | number
How can I narrow the type without further checking the method property? (Likely changing the Fn type)
CodePudding user response:
When having trouble with generic functions, the first thing I usually refactor it until the generic argument is naked.
That might look like this:
type Fn = <A extends Omit<Actions, 'response'>>(a: A) =>
Extract<Actions, { method: A['method'] }>;
So here A
is the input type only. And for the output, you just transform the type you need from that input type. In this case, extract from the original union type the member with { method: A['method'] }
.
This makes it easy for typescript to find the type of A
, since it doesn't have to try an reverse your type transformations.
CodePudding user response:
The root cause of the problem is that the function call
fn({ method: "connections", request: 10 })
is typed:
const fn: <Actions>(a: Pick<Actions, "method" | "request">)
That is, the type parameter A
is inferred with Actions
, the union of all possible Action
types, not the particular Action
type being invoked. This has ramifications well beyond a vague return type. For instance:
fn({ method: "connections", request: "weird" })
will compile just fine, because the type of request, A['request']
, resolves to Action['request']
, which is number | string
, which admits "weird"
...
I don't know a way to fix this with the action specification you have provided, but if we describe the possible actions like this:
type Actions = {
connections: {
request: number,
response: number,
},
delete: {
request: string,
response: string,
}
}
the following works as intended:
type Fn = <M extends keyof Actions>(a: {method: M, request: Actions[M]['request']}) => {method: M, response: Actions[M]['response']};
declare const fn: Fn;
fn({ method: "connections", request: "weird" }); // Error: Type 'string' is not assignable to type 'number'
const x = fn({ method: "connections", request: 42 }); // ok
x.response // type number