children prop을 가지는 컴포넌트를 만들 때 React에서 제공하는 PropsWithChildren
타입을 애용했다. 예를 들어, children과 foo를 prop으로 가지는 컴포넌트는 다음과 같다.
function Foo({ foo, children }: PropsWithChildren<{ foo: string }>) {
return (
<div>
{foo}
{children}
</div>
)
}
children만 prop으로 가지는 컴포넌트는 PropsWithChidren<{}>
을 사용했다.
참고로 PropsWithChildren
은 이렇게 생겼다.
type PropsWithChildren<P> = P & {
children?: ReactNode | undefined;
}
그런데, @typescript-eslint/ban-types 규칙이 {}
를 쓰지 말라고 했다. 나는 이 타입이 빈 객체를 의미하는 타입일 줄 알았는데... 실제 작동은 null이 아닌 값 전체를 나타낸다고 한다. null이 아닌 값 전체와 { children }
의 intersection은 { children }
이니, 유효했던 것이었다. 그래도 {}
와 이별하기 위해 PropsWithChildren
에 넣을 다른 값을 알아내는 여정을 시작했다.
첫 번째 시도는 엄격한 빈 객체 타입이다. Record<string, never>
로 하면 진짜 빈 객체를 나타내니 문제가 쉽게 해결될 것이다. 하지만 실제로는 사용할 수 없는 컴포넌트가 된다. 컴포넌트 선언에선 문제가 없지만, 컴포넌트를 사용할 때 index signature와 children의 타입이 호환이 되지 않는다는 오류가 발생한다. string인 index 시그니처에 들어가는 값은 never라서 ReactNode를 할당할 수 없는 것이다.
function Type1({ children }: PropsWithChildren<Record<string, never>>) {
return <div>{children}</div>
}
<Type1>asdf</Type1> // { children: string }에는 index signature와 호환이 안되는 문제 발생
두 번째 시도는 임의의 객체였다. Record<string, unknown>
을 사용하면 인덱스 시그니처가 호환될 것이다. 하지만 이 컴포넌트는 다른 문제가 있었다. 임의의 속성을 갖는 객체를 사용했으므로 임의의 prop을 사용할 수 있게 되어버렸다. 타입 시스템을 해치는 일이기 때문에 다른 타입을 찾게 되었다.
function Type2({ children }: PropsWithChildren<Record<string, unknown>>) {
return <div>{children}</div>
}
<Type2>asdf</Type2> // OK
<Type2 foo="HACK">asdf</Type2> // OK...이지 않았으면 한다.
파라미터로 넣는 타입과 { children }
과 intersection을 한 뒤에 { children }
이 남으려면 어떤 값이어야 할까? A 교집합 B를 했는데 A가 나오면 B는...? 전체 집합이다. TypeScript 세계에선 그걸 any
라고 부른다. 그런데 any
도 친해지면 안 되니까 unknown
을 썼다. unknown
을 쓰면 children을 문제 없이 사용할 수 있으면서도 임의의 prop을 허용하지 않는 컴포넌트를 만들 수 있다.
function Type3({ children }: PropsWithChildren<unknown>) {
return <div>{children}</div>
}
<Type3>asdf</Type3> // OK
<Type3 foo="HACK">asdf</Type3> // Not OK