import { ElementType, ReactElement } from 'react'; /** * Creates a Prop Types from a VariantRecord. * Function variants get the type inferred by the first argument type */ type EvaluateProps = { [Prop in keyof Variants]: Variants[Prop] extends Record ? keyof Variants[Prop] : Variants[Prop] extends (...args: any) => any ? Parameters[0] : never; }; type EvaluateRecordProps = { [Prop in keyof Variants]: Variants[Prop] extends Record ? keyof Variants[Prop] : Variants[Prop] extends (arg: any) => any ? any : never; }; /** * Gets the keys that 2 types have in common * GetIntersectionKeys<{ x, y, z }, { h, x, z }> = 'x' | 'z' */ type GetIntersectionKeys = { [k in keyof T]: K extends k ? K : never; }[keyof T]; /** * Create Optional Props type. * CreateOptionalProps<{ x: string, y: number}, 'x'> = { x?: string } */ type CreateOptionalProps = Partial< Pick> >; type EvaluatePropsWithDefaultVariants< Variants extends VariantsRecord, DefaultVariants > = Omit, keyof DefaultVariants> & CreateOptionalProps, DefaultVariants>; /** * Variants can receive records or functions. This is the function type of it. * It would be incredible if we could pass the types to it, but it would need to auto-infer * the variants as it's been written. */ type ClassNameFunction = (value: any, props: any, variants: any) => string; export type VariantsRecord = Record< string, Record | ClassNameFunction >; export type CompoundVariants< Variants extends VariantsRecord, Keys extends keyof Variants = keyof Variants > = Partial< { [k in Keys]: keyof Variants[k]; } > & { defaultTo?: Partial< { [k in Keys]: keyof Variants[k]; } >; class?: string; }; /** * Configuration to create a Component with variants */ export interface ComponentConfig< Variants extends VariantsRecord, DefaultVariants, DefaultAs extends ElementType > { className?: string; variants?: Variants; transient?: (keyof Variants)[]; defaultVariants?: DefaultVariants; compoundVariants?: Array>; defaultProps?: Partial< InferAnyComponentProps> >; } /** * Get Default Values types. * It could be better, infering which values it could receive, like using * = Partial> * But, that breaks `styled`, as it makes all props optional. */ type GetDefaultVariants = Partial< EvaluateRecordProps >; /** * Styled Component. Supports the As prop, which changes the prop types based on the component * it's rendering. */ export interface Component< DefaultAs extends ElementType, Variants extends VariantsRecord, DefaultVariants > { ( props: StyledProps ): ReactElement; displayName?: string | undefined; } /** * Extract Props from a Component */ type InferAnyComponentProps = T extends ElementType ? Props : T; type ToIntrinsicElementIfPossible = As extends keyof JSX.IntrinsicElements ? JSX.IntrinsicElements[As] : As; /** * Extract Props from a W.Component */ export type Infer = T extends Component< infer DefaultAs, infer Variants, infer DefaultVariants > ? StyledProps : InferAnyComponentProps; type GetPropsWithoutVariantsKeys< As extends ElementType, Variants extends VariantsRecord > = Omit< InferAnyComponentProps>, keyof Variants | 'as' >; /** * Evaluates props based on Variants. Variants that have a Default value become optional. * It also merges with the Component's Props, replacing the ones that are variants. * Ex.: * const Component = styled( * ({ color, checked, x }: { color: string, checked: string, x: string }) => null, { * variants: { * color: { * gray: 'foo', * red: 'bar', * }, * checked: (value: boolean) => value ? 'x' : 'y' * }, * defaultVariants: { * color: 'gray' * } * }) * * // ✅ * // ❌ missing `x` prop * // ❌ `x` prop should be string * // ❌ `checked` prop should be boolean * // ❌ missing `checked` prop * // ✅ */ export type StyledProps< As extends ElementType, Variants extends VariantsRecord, DefaultVariants > = { as?: As } & GetPropsWithoutVariantsKeys & EvaluatePropsWithDefaultVariants; /** * Styled Function should infer the Component, the Variants object and the Default * Variants values so the created component has the right props. */ interface StyledFunction { < DefaultAs extends ElementType, Variants extends VariantsRecord = {}, DefaultVariants extends GetDefaultVariants = {} >( component: DefaultAs, config: ComponentConfig ): Component; } /** * Styled should be callable by: * styled('a', { className: 'text-sm', variants: { ... } }) * styled(Component, { className: '...', variants: { ... } }) * styled.a('text-sm', { variants: { ... } }) * styled.a('text-sm') */ export type Styled = StyledFunction & { [DefaultAs in keyof JSX.IntrinsicElements]: StyledTagFunction; }; export type StyledTagFunction = < Variants extends VariantsRecord = {}, DefaultVariants extends GetDefaultVariants = {} >( cx: string, config?: Omit< ComponentConfig, 'className' > ) => Component; /** * Configuration to create a Component with variants */ export interface ClassNameFactorConfig< Variants extends VariantsRecord, DefaultVariants > { className?: string; variants: Variants; defaultVariants?: DefaultVariants; compoundVariants?: Array>; } export type ClassNameFactor = { < Variants extends VariantsRecord, DefaultVariants extends Partial> >( config: ClassNameFactorConfig ): ( props: EvaluatePropsWithDefaultVariants ) => string; };