Home > database >  Discriminating Union Types by unique property
Discriminating Union Types by unique property

Time:11-29

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.

See Playground

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
  • Related