Home > OS >  How to make a function overload generic to strongly type its implementation parameters?
How to make a function overload generic to strongly type its implementation parameters?

Time:06-25

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.

Playground link to code

  • Related