Optional generic type and corresponding parameters for TypeScript

Viewed 2076

I have the following React functional component that takes generic parameters:

type Props<T, S> = {
  data: T[]
  additionalData: S
}

function Component<T, S = void>({
 ....
 

You can use it as follow:

<Component<Bike, BikeShed> data={bikes} additionalData={bikeShed} />

Since the second generic parameter is optional, you have to use it as follows:

<Component<Car> data={cars} additionalData={undefined} />

Now here is the question. Is there a way to omit the additionalData={undefined} because it is clearly not being used here.

What I tried is this:

type Props<T, S = void> = {
  data: T[]
  additionalData?: S
}

But it effectively changes S to S | undefined, not an ideal situation!

Can I set up my Props in a way where I don't have to specify its relating properties when the optional generic parameter is void?

2 Answers

Yep - you can check if S is void and if so evaluate to a type without additionalData using conditional types:

type BaseProps<T> = { data: T[] };
type PropsWithAdditionalData<T, S> = BaseProps<T> & { additionalData: S };
type Props<T, S = void> = S extends void ? BaseProps<T> : PropsWithAdditionalData<T, S>;

const hasAdditionalData = <T, S>(props: BaseProps<T> | PropsWithAdditionalData<T, S>): props is PropsWithAdditionalData<T, S> => {
  return !!(props as unknown as PropsWithAdditionalData<T, S>).additionalData;
}

function Component<T, S = void>(props: Props<T, S>) {
  if (hasAdditionalData<T, S>(props)) {
    return <div>{props.additionalData}</div>;
  }
  return <div>{props.data}</div>;
}

type Car = { id: number };
const cars = [{ id: 1 }, { id: 2 }];

const noAdditionalData = <Component<Car> data={cars} />;
const additionalData = <Component<Car, number> data={cars} additionalData={1} />;

S extends void checks if the type S is assignable to void (which is is in the default case) and if so returns just { data: T[] }.

Your Props type should be strict union type.

import React, { FC } from 'react'

// credits goes https://stackoverflow.com/questions/65805600/type-union-not-checking-for-excess-properties#answer-65805753
type UnionKeys<T> = T extends T ? keyof T : never;
type StrictUnionHelper<T, TAll> =
    T extends any
    ? T & Partial<Record<Exclude<UnionKeys<TAll>, keyof T>, never>> : never;

type StrictUnion<T> = StrictUnionHelper<T, T>

type Props<T, S> = StrictUnion<{
    data: T[]
} | {
    data: T[]
    additionalData: S
}>

type Bike = {
    color: string
}

type BikeShed = {
    model: string
}
const Foo = <T, S = void>(props: Props<T, S>) => {
    const { additionalData } = props // ok, but may be undefined
    const { data } = props; // ok

    return null
}

const bikes: Bike[] = []
const bikeShed = { model: '' }

const component = <Foo<Bike, BikeShed> data={[]} />

Playground

More information about handling union props in React You can find in my blog

UPDATE

You can also try typeguards:

import React, { FC } from 'react'

type WithoutAdditional<T> = {
    data: T[]
}
type WithAdditional<T, S> = {
    data: T[]
    additionalData: S
}

type Props<T, S> =
    | WithAdditional<T, S>
    | WithoutAdditional<T>

const hasProperty = <Obj, Prop extends string>(obj: Obj, prop: Prop): obj is Obj & Record<Prop, unknown> =>
    Object.prototype.hasOwnProperty.call(obj, prop)

const withAdditional = <T, S>(obj: Props<T, S>): obj is WithAdditional<T, S> => hasProperty(obj, 'additionalData')

type Bike = {
    color: string
}

type BikeShed = {
    model: string
}
const Foo = <T, S = void>(props: Props<T, S>) => {
    if (withAdditional(props)) {
        const x = props; // WIthAdditional

    } else {
        const y = props; // WIthoutAdditional
    }

    return null
}

const bikes: Bike[] = []
const bikeShed = { model: '' }

const component = <Foo<Bike, BikeShed> data={[]} />

Playground 2

Related