공통 컴포넌트를 만들면서 radix-ui의 Slot 컴포넌트를 잘 사용하고 있는데, 이를 사용해서 컴포넌트를 만들게 되면
const { asChild, ...primitiveProps } = props
const Comp: any = asChild ? Slot : node
return <Comp>...
이런 식으로 매번 asChild에 대한 타입과, 분기처리 로직이 반복되어 들어가야 해서 귀찮았는데 이를 radix-ui에서는 어떻게 처리하는지 살펴봤는데 이를 Primitive 라는 컴포넌트로 해결하고 있었다
그래서 직접 다운받아 사용하려고 봤더니 radix-ui 내부 로직이 적용되어 있어서 직접 옮겨서 사용하려다 타입 부분이 이해가 안가서 찾아봤다
import * as React from "react"
import { Slot } from "@radix-ui/react-slot"
const NODES = [
"a",
"button",
"div",
"form",
"h2",
"h3",
"img",
"input",
"label",
"li",
"nav",
"ol",
"p",
"span",
"svg",
"ul",
] as const
type Primitives = {
[E in (typeof NODES)[number]]: PrimitiveForwardRefComponent<E>
} //key: Nodes들 , value: ref forwarding한 컴포넌트
type PrimitivePropsWithRef<E extends React.ElementType> =
React.ComponentPropsWithRef<E> & {
asChild?: boolean
}
interface PrimitiveForwardRefComponent<E extends React.ElementType>
extends React.ForwardRefExoticComponent<PrimitivePropsWithRef<E>> {}
const Primitive = NODES.reduce((primitive, node) => {
const Node = React.forwardRef(
(props: PrimitivePropsWithRef<typeof node>, forwardedRef: any) => {
const { asChild, ...primitiveProps } = props
const Comp: any = asChild ? Slot : node
return <Comp {...primitiveProps} ref={forwardedRef} />
},
)
Node.displayName = `Primitive.${node}`
return { ...primitive, [node]: Node }
}, {} as Primitives)
const Root = Primitive
export { Primitive, Root }
export type { PrimitivePropsWithRef }
자주 사용하는 DOM elements의 문자열 리터럴들을 배열에 등록해주고,
reduce를 통해 각 element마다 위에서 해줬던 처리(asChild,Comp)를 해주고, ref forwarding까지 해준 뒤 key는 element, value는 컴포넌트 형태로 Primitive 라는 객체에 저장한다 (이 Primitive의 타입은 Primitives 이다)
이제 컴포넌트를 사용할때 Primitive.button 이런 식으로 사용하면 된다
컴포넌트의 props type은 PrimitivePropsWithRef<E extends React.ElementType> 인데,
이는 React.ComponentPropsWithRef<E> 에 {asChild?:boolean} 을 추가한 타입이다
이 객체의 타입을 Primitives라는 타입인데
type Primitives = {
[E in (typeof NODES)[number]]: PrimitiveForwardRefComponent<E>
}
key의 경우 nodes 배열에 있는 element이고, value(컴포넌트 타입)의 경우
PrimitiveForwardRefComponent<E> 인데
extends React.ForwardRefExoticComponent<PrimitivePropsWithRef<E>> 이다
이 중에서 PrimitivePropsWithRef<E> 는 위에서 살펴봤고, React.ForwardRefExoticComponents 는 ref forwarding된 컴포넌트의 리턴 타입이다
export interface SelectTriggerProps
extends ComponentPropsWithoutRef<typeof Primitive.button> {
placeholder?: string
}
export const SelectTrigger = forwardRef<
React.ElementRef<typeof Primitive.button>,
SelectTriggerProps
>((props, ref) => {
return <Primitive.button {...props}/>
Primitive 컴포넌트를 사용할 거라면 props type은 ComponentPropsWithoutRef<typeof Primitive.element> 을 확장하여 사용하면 되는데,
여기서 어떻게 ref forwarding된 컴포넌트 타입에서 props의 타입만 빼올 수 있는걸까? 궁금해져서 알아봤다
제일 먼저 ComponentPropsWithoutRef 를 찾아보면
type ComponentPropsWithoutRef<T extends ElementType> = PropsWithoutRef<ComponentProps<T>>;
여기서 또 궁금해지는 건
typeof Primitive.button 이 어떻게 ElementType 에 할당 가능할까?'div'|'span' 이걸로만 알고 있었다type ElementType<P = any, Tag extends keyof JSX.IntrinsicElements = keyof JSX.IntrinsicElements> =
| { [K in Tag]: P extends JSX.IntrinsicElements[K] ? K : never }[Tag]
| ComponentType<P>;
ElementType에 제네릭을 명시하지 않으면 ElementType<any,keyof JSX.IntrinsicElements> 가 되고,
keyof JSX.IntrinsicElements는 존재하는 html element('div','span')들의 유니온타입
P는 any이기 때문에 P extends JSX.IntrinsicElements[K] 을 만족한다
{ [K in Tag]: P extends JSX.IntrinsicElements[K] ? K : never } 는
{div : 'div', span : 'span'} 이런 식의 타입이 되고
여기에 [Tag] 로 접근하면 최종적으로는 이런 식이 된다
type ElementType =
| 'div' | 'span' | 'button' | //html 태그
| ComponentType<any> // React 컴포넌트 타입
하지만 Primitive.button 은 첫번째가 아닌 ComponentType<any> 를 충족하기 때문에 ElementType을 만족한다
ComponentPropsWithoutRef<typeof Primitive.button> 이 Primitive.button 의 props type을 추론할 수 있을까? type ComponentPropsWithoutRef<T extends ElementType> = PropsWithoutRef<ComponentProps<T>>;
PropsWithoutRef<ComponentProps<T>> 에서 ComponentProps를 먼저 보면 주석이 적혀있는데
* Used to retrieve the props a component accepts with its ref. Can either be
* passed a string, indicating a DOM element (e.g. 'div', 'span', etc.) or the
* type of a React component.
ref와 함께 컴포넌트의 props를 추출하는데, DOM element의 string이나, React component의 타입이 가능하다고 써 있다
type ComponentProps<T extends keyof JSX.IntrinsicElements | JSXElementConstructor<any>> = T extends
JSXElementConstructor<infer P> ? P
: T extends keyof JSX.IntrinsicElements ? JSX.IntrinsicElements[T]
: {};
typeof Primitive.button 은 두번째 JSXElementConstructor<any> 에 해당JSXElementConstructor<any> 는 props => component 같은 함수 타입)T extends JSXElementConstructor<infer P> ? P 를 통해 P는 PrimitivePropsWithRef<'button'> -> primitive.button의 props type을 추론한다PropsWithoutRef 는 ref를 제외해주는 타입이다type PropsWithoutRef<P> =
P extends any ? ("ref" extends keyof P ? Omit<P, "ref"> : P) : P
ComponentProps 라는 타입에서 infer를 통해 (ref forwarding이 적용되었더라도) 컴포넌트의 props type만 추출할 수 있다keyof JSX.IntrinsicElements 를 한번 고려해보는 것도 좋을 것 같다