pushy
pushy

Reputation: 41

TypeScript Conditionally Merge Prop Types Using Generics

Compiler tells me that properties propA and propB inside TestComponent don't exist on type Props<T>. Is there something I'm missing or misunderstanding about conditional types?

import React from 'react';

type PropsBase<T extends boolean | undefined> = {
  isA?: T;
};

type PropsA = {
  propA: string;
};

type PropsB = {
  propB: string;
};

type Props<T extends boolean | undefined> = PropsBase<T> & (T extends false | undefined ? PropsB : PropsA);

function TestComponent<T extends boolean | undefined = true>(props: Props<T>) {
  if (props.isA) {
    return <>{props.propA}</>; // Property 'propA' does not exist
  }
  if (!props.isA) {
    return <>{props.propB}</>; // Property 'propB' does not exist
  }

  return <></>;
}

<>
  <TestComponent propA="propA" /> // Should be valid
  <TestComponent isA propA="propA" /> // Should be valid
  <TestComponent isA={false} propB="propB" /> // Should be valid
  <TestComponent isA propB="propB" /> // Should be invalid
</>

My goal was to create an extendable and reusable type which properties can be controlled through generics. I know this is also done with unions, but it's not quite easy to build other types on top of it.

Playground link.

Upvotes: 1

Views: 207

Answers (1)

Kelvin Schoofs
Kelvin Schoofs

Reputation: 8718

I think your first example of <TestComponent propA="propA" /> should also be invalid? After all, in that case, ìsAis of typeundefined`.

I changed your Props<T> into this Props instead:

type Props = ({ isA: true } & PropsA) | ({ isA?: false } & PropsB);

function TestComponent(props: Props) {
    if (props.isA) {
        return props.propA; // OK
    }
    if (!props.isA) {
        return props.propB; // OK
    }
}

Your usage of it is also validated properly now:

TestComponent({ propA: 'propA' }); // Property 'isA' is missing
TestComponent({ propB: 'propB' }); // OK
TestComponent({ isA: true, propA: 'propA' }); // OK
TestComponent({ isA: false, propB: 'propB' }); // OK
TestComponent({ isA: true, propB: 'propB' }); // 'propB' does not exist

Interestingly, if I use prop.isA === true, TypeScript behaves better:

function TestComponent(props: Props) {
    if (props.isA === true) {
        return props.propA; // OK
    }
    const isA = props.isA; // type `false | undefined`
    if (!props.isA) {
        return props.propB; // OK
    }
}

That should be good enough in your case I hope? Although I'm surprised in the difference between if (props.isA) and if (props.isA === true). Mind that this requires TypeScript to run in strict mode, with noImplicitAny enabled to be specific. Otherwise you need to use if (props.isA === true). Weird, but running with noImplicitAny disabled is very rare anyway.

Upvotes: 1

Related Questions