I am trying to implement a Typography
component in React, and need to make it flexible enough, so it will as:
variant
prop is mandatorythe
as
prop is optionala. if
as
is a string, it will be a native html tag and surface this tag's propertiesb. if
as
is a react element, it will surface the react element's propertiesc. 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]:
- 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>
);