import * as React from "react"; import { COMPONENT_TYPE, DEFAULT_TEMPLATE_AS, SLOT_NAME, TEMPLATE_TYPE_IDENTIFIER, SLOT_TYPE_IDENTIFIER, type DefaultSlotName, } from "./constants"; import { Pretty, UnionToIntersection } from "./typeUtils"; // Note on {} vs object in type args. // We like to write `extends object` whenever a type is exported because it's // bit more stricter to what can actually be assigned to it. // For utility types that don't get exported but are used by other types, // we prefer {} because it looks cleaner on hover type ElementType = | Exclude, SlotComponent> | Exclude>; // In edition to merging props and overriding children, // this type also manages to maintain whether children in original type // was defined as required or optional argument type MergeTPropsAndTAsProps< TProps extends {}, TAsProps extends {}, _TAsProps extends {} = 0 extends TAsProps & 1 ? { children?: React.ReactNode } : TAsProps, > = { [K in keyof _TAsProps]: K extends "children" ? // Only allow children of the as element to be ReactNode. // This is done because slots change the children before passing it to // the initialized as element. _TAsProps[K] extends React.ReactNode ? _TAsProps[K] | ((props: TProps) => _TAsProps[K]) : never : _TAsProps[K]; }; type TemplateProps< TProps extends {}, TAs extends ElementType | SlotComponent, TAsProps extends TAs extends SlotComponent ? TProps : {}, > = { as?: TAs } & (TAs extends SlotComponent ? // When we have: // // This makes sure that props that child (otherComponent) specified is optional (can be overridden) // but the extra props that's specified in parent component's slot type is required to provide { children?: React.ReactNode; } & Partial & Omit : MergeTPropsAndTAsProps); type UnwrapProps = T extends SlotComponent< infer TProps > ? TProps : React.ComponentPropsWithoutRef; export type TemplateComponent = { = typeof DEFAULT_TEMPLATE_AS>( // Typescript is failing to enforce `UnwrapProps extends TProps` so // the next best thing is to wrap the type in another conditional type // and provide a custom error. // It's probably a ts bug because TemplateProps on it's own works as expected // UnwrapProps can be any, so to avoid props inferred as string, we guard the distributivity by wrapping it in [] props: [UnwrapProps] extends [ TAs extends SlotComponent ? TProps : any, ] ? TemplateProps> : "Custom error: Props of `SlotComponent<:Props:>` must extend props of `TemplateComponent<:Name:, :Props:>` when used for `as` prop", ): React.ReactElement | null; [SLOT_NAME]: TName; [COMPONENT_TYPE]: typeof TEMPLATE_TYPE_IDENTIFIER; }; export type TemplateComponentLikeElement< TName extends string, TProps extends {}, > = React.ReactElement< { children?: React.ReactNode | ((props: TProps) => React.ReactNode); as?: React.ElementType; }, { (props: any): any; [SLOT_NAME]: TName; [COMPONENT_TYPE]: typeof TEMPLATE_TYPE_IDENTIFIER; } >; export type TemplateAsSlotComponentLikeElement< TName extends string, TProps extends {}, > = React.ReactElement< { children?: React.ReactNode; as?: SlotComponent; }, { (props: any): any; [SLOT_NAME]: TName; [COMPONENT_TYPE]: typeof TEMPLATE_TYPE_IDENTIFIER; } >; type SlottableNode = | TemplateComponentLikeElement | TemplateAsSlotComponentLikeElement | React.ReactElement<{ "slot-name": TName }> | (DefaultSlotName extends TName ? (props: TProps) => React.ReactNode : never) | (DefaultSlotName extends TName ? string | number | boolean | null | undefined : never); // --------------- // type SlotTuple = [TName, TProps]; type SlotTupleToSlottableNode> = T extends SlotTuple ? SlottableNode : never; /** * Removes the disallowed props without cluttering the type declaration. * Compared to Omit which shows up on hover */ type OmitDisallowedProps = T extends | { children?: any } | { key?: any } | { as?: any } | { ref?: any } | { "slot-name"?: any } ? { [P in Exclude< keyof T, "children" | "as" | "key" | "ref" | "slot-name" >]: T[P]; } : T; type DistributiveKeyOf = T extends unknown ? keyof T : never; export type Slot< TName extends string | undefined | object = undefined, TProps extends object = {}, > = { value: 0 extends TName & 1 ? SlotTuple : TName extends object ? SlotTuple> : SlotTuple< TName extends undefined ? DefaultSlotName : Exclude, 0 extends TProps & 1 ? any // only true when TProps is any. Important for providing Base type to extend from : DistributiveKeyOf> extends never ? {} // When an empty object (or object with disallowed keys) are passed, change the type to `{}` because it looks clearer on hover : TProps extends unknown ? OmitDisallowedProps : never >; }; type MergeDuplicateSlots< U extends SlotTuple, _U extends SlotTuple = U, > = U extends unknown ? SlotTuple[1]> : never; // Moving recursion outside helps display SlotChildren<...> consistently as SlottableNode<..., ...> on hover. // Sometimes this shows up in the type declaration on hover, hence why the ambiguous name // FIXME: Something used to break if Iterable was used instead of an array but can't remember what, although // using iterable makes it so that ReactNode is assignable to SlotChildren type Children> = T | Iterable>; type UnwrapValue> = T["value"]; type AnyCase = 0 extends TSlot & 1 ? Slot : TSlot; /** * Declares the type of children based on slots. * Provide the type union of `Slot` to accept different types of slotted children. * @example * ```ts * // The following type accepts children with three types of slots * type Children = Slots | Slot<"bar", {baz: boolean}>> * ``` */ export type SlotChildren = Slot> = | SlotTupleToSlottableNode>>> | Children< SlotTupleToSlottableNode>>> >; /** * @example * ```ts * type X = GetTemplateUnions | Slot<"bar", {baz: string}>>> * // | {default: TemplateComponent<"default", {}> * // | {foo: TemplateComponent<'foo', {}> * // | {bar: TemplateComponent<'bar', {baz: string}>} * ``` */ type GetTemplateUnions = U extends TemplateAsSlotComponentLikeElement< infer N, infer P > ? { [Name in Exclude]: TemplateComponent } : never; /** * Create type-safe template */ export type CreateTemplate = Pretty< UnionToIntersection< GetTemplateUnions> > >; // ------------------ // export type SlotProps = TProps & { children?: React.ReactNode; }; // If all properties on P are optional then defaultContent and props can be optional, // Otherwise both should be specified export type SlotComponent

= { [COMPONENT_TYPE]: typeof SLOT_TYPE_IDENTIFIER; } & ([Partial

] extends [P] ? { /** Slot Component */ (props: SlotProps

): React.ReactElement | null; /** Slot Function */ ( defaultContent?: React.ReactNode, props?: P & { key?: React.Key }, key?: React.Key, ): React.ReactElement | null; } : { /** Slot Component */ (props: SlotProps

): React.ReactElement | null; /** Slot Function */ ( defaultContent: React.ReactNode, props: P & { key?: React.Key }, key?: React.Key, ): React.ReactElement | null; }); type GetSlotComponentUnions = T extends TemplateAsSlotComponentLikeElement< infer N, infer P > ? { [Name in N]: SlotComponent

} : never; type CreateSlotComponent = {} & Pretty< UnionToIntersection< GetSlotComponentUnions<0 extends T & 1 ? SlotChildren : T> > >; export type CreateSlot = CreateSlotComponent; // -------------------- // export type HasSlot = Pretty<{ [Name in Extract< T, React.ReactElement<{ "slot-name": string }> >["props"]["slot-name"]]: true | undefined; }>; // -------------------- //