skip to Main Content

Say I have an object defining my Typography components color and shade.

const theme = {
  primary: [10, 20, 50, 100],
  secondary: [20, 30, 80],
  accent: [10, 20]
}

Typography should accept 2 props, color and shade.

Type definition for color can be: keyof typeof theme.

How do I specify the type of shade prop such that it only accepts the value within the array of the palette defined in the theme object? For instance, if color is accent, shade cannot be 30. It can only be 10 or 20.

Code snippet:

function Typography({
  color,
  shade,
  children,
}: {
  color: keyof typeof theme;
  shade: ???;
  children: React.ReactNode;
}) {
  return <div>{children}</div>;
}

function Main() {
  return (
    <Typography 
      color="primary" 
      shade={}>
        Some text
    </Typography>
  );
}

Codesandbox – https://codesandbox.io/s/conditional-prop-types-cwy49q

2

Answers


  1. You need to use a generic and infer what shade could be from that generic:

    const theme = {
      primary: [10,20,50,100],
      secondary: [20,30,80],
      accent: [10,20]
    } as const
    
    type Theme = typeof theme
    
    type Props<Color extends keyof Theme> = {
      color: Color
      shade: Theme[Color][number]
    }
    
    
    const MyFunc = <Color extends keyof Theme>({
      color,
      shade
    }: Props<Color>) => {
      // stuff here
    }
    
    
    // fails
    MyFunc({color:'secondary',shade:10})
    
    // works!
    MyFunc({color:'secondary',shade:20})
    
    

    Typescript Playground

    Login or Signup to reply.
  2. There are two possible approaches:

    1. Using generics
    2. Create a union of all possibilities (not recommended for big amount of values)

    Either of the approaches will require a change in theme.

    Currently, the type of the theme is Record<string, number[]> which is not suitable for us. We have to prevent the compiler from widening types to their primitives.

    This can be done by using const assertion:

    const theme = {
      primary: [10, 20, 50, 100],
      secondary: [20, 30, 80],
      accent: [10, 20],
    } as const;
    

    The problem with const assertion is that we lose type checking and if you use Typescript >= 4.9 it can be fixed with satisfies operator:

    const theme = {
      primary: [10, 20, 50, 100],
      secondary: [20, 30, 80],
      accent: [10, 20],
    } as const satisfies Record<string, readonly number[]>;
    

    Since const assertion converts everything to readonly it is crucial to write readonly number[] in satisfies instead of just number[].

    Let’s create a type for the theme:

    // type Theme = {
    //     readonly primary: readonly [10, 20, 50, 100];
    //     readonly secondary: readonly [20, 30, 80];
    //     readonly accent: readonly [10, 20];
    // }
    type Theme = typeof theme;
    

    Approach with generics:

    Typography will accept a generic parameter constrained by the keyof Theme and by using it we will get the values for shade using indexed access:

    function Typography<T extends keyof Theme>({
      color,
      shade,
      children,
    }: {
      color: T;
      shade: Theme[T][number];
      children: React.ReactNode;
    }) {
      return <div>{children}</div>;
    }
    

    Approach with the union of possible values:

    By using mapped types we will map through Theme and generate a union of {color: string, shade: number}:

      type PossibleValuesUnion = {
        [K in keyof Theme]: { color: K, shade: Theme[K][number] }
      }[keyof Theme]
    

    Usage:

     function Typography({
        color,
        shade,
        children,
      }: PossibleValuesUnion & {
        children: React.ReactNode;
      }) {
        return <div>{children}</div>;
      }
    

    Either of these should do the trick, personally, I would recommend the first approach if the theme will grow bigger.

    playground

    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search