{} 타입 안 쓰기

박기완·2022년 1월 14일
2

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

TS Playground

0개의 댓글