Home > Software design >  Conditional React component properties with multiple generics
Conditional React component properties with multiple generics

Time:01-12

I am trying to implement a Typography component in React, and need to make it flexible enough, so it will as:

  1. variant prop is mandatory

  2. the as prop is optional

    a. if as is a string, it will be a native html tag and surface this tag's properties

    b. if as is a react element, it will surface the react element's properties

    c. if as is not defined, it will default to the variant's mapped element

For example if as="a", we expect that href property will exist.

The example code is below, which works when testing the type Proxy only but using it with the react component it is not working:

import React from "react";

const TypographyDefaultMapping = {
    h1: 'h1',
    h2: 'h2',
    h3: 'h3',
    'body-medium': 'span',
    label: 'span',
} as const;

export type TypographyVariants = keyof typeof TypographyDefaultMapping;

type Tags = keyof HTMLElementTagNameMap;

type AsType = undefined | Tags | React.FunctionComponent<any>;

type VariantsDefaultType<T extends TypographyVariants> = Omit<
    React.ComponentPropsWithoutRef<typeof TypographyDefaultMapping[T]>,
    'as'
> & {
    variant: TypographyVariants;
};

export type NewConditionalTypography<
    T extends AsType,
    V extends TypographyVariants
> = T extends undefined
    ? VariantsDefaultType<V> // Undefined "as" property, fallback to variant's default
    : T extends Tags
    ? {
        variant: TypographyVariants;
        as: Tags;
        bloup: boolean;
    } & React.ComponentPropsWithoutRef<T> // "as" property is a valid HTML tag
    : T extends React.FunctionComponent<any> // "as" property is a React element
    ? {
        variant: TypographyVariants;
        as: React.FunctionComponent<any>;
        bib: boolean;
    } & React.ComponentProps<T>
    : never;

type Proxy<T = unknown> = T extends {
    as: infer As extends AsType;
    variant: infer Var extends TypographyVariants;
}
    ? NewConditionalTypography<As, Var>
    : T extends {
        variant: infer Var extends TypographyVariants; // case of not defined "as"
    }
    ? NewConditionalTypography<undefined, Var> 
    : never;

const NewTypography = (props: Proxy) => (
    <div>Test: {JSON.stringify(props)}</div>
);

const Link = ({ linkClick }: { linkClick: () => void }) => (
    <button onClick={linkClick}>Test</button>
);

// Expected to get HTMLHeadingElement props   variant prop
type TestProxy = Proxy<{ variant: 'h2' }>;
     // ^?

// Expected to get HTMLSpanElement props   variant prop
type TestProxySpan = Proxy<{ variant: 'body-medium' }>;
     // ^?

// Expected to get HTMLAnchor props   variant prop
type TestProxy2 = Proxy<{ variant: 'h2'; as: 'a' }>;
     // ^?

// Expected to get Link element props   variant prop
type TestProxy3 = Proxy<{ variant: 'h2'; as: typeof Link }>;
     // ^?

const TestingComponent = () => (
    <div>
    // Should allow (and auto-complete) "href"
        <NewTypography variant="h2" />
    // Should allow (and auto-complete) "linkClick"
        <NewTypography
            variant="body-medium"
            as={Link}
        //   linkClick={() => console.log('Test')}
        />
    // Should not allow "href"
        <NewTypography variant="body-medium" as="span" href="www.example.com" />
    </div>
);

or as a Typescript Playground link.

Any clue how can achieve this, and if it's feasible after all?

[Edit]:

  1. Simplified playground based on comments

CodePudding user response:

You need to add the type parameters to the function so they can be inferred. After that some minor tweaks to NewConditionalTypography and it should work:



export type NewConditionalTypography<
    T extends AsType,
    V extends TypographyVariants
> = // Undefined "as" property, fallback to variant's default
    T extends undefined? VariantsDefaultType<V>: 
    // "as" property is a React element
    T extends (p: any) => JSX.Element ? { bib: boolean; } & React.ComponentProps<T>: 
    // "as" property is a valid HTML tag
    T extends Tags ? { bloup: boolean; } & React.ComponentPropsWithoutRef<T> :
    {}

const NewTypography = <TVar extends TypographyVariants, TAs extends AsType = undefined>(props: {
    as?: TAs;
    variant: TVar;
} & NewConditionalTypography<TAs, TVar> ) => (
    <div>Test: {JSON.stringify(props)}</div>
);

Playground Link

  • Related