type 선언을 통해 children 특정 컴포넌트만 허용하기

pengooseDev·2023년 9월 20일
1
post-thumbnail

??? : 소..솔직히.. ReactNode는.. any type이랑.. 크... 크게 다르지 않다고.. 생각해요...


노마드코더방에 좋은 질문이 올라왔다.

React.ReactNode type은 사실상 모든 type을 거의 다 받는다. 사실상 컴포넌트계의 any와 다름이 없다. 어떻게하면 한정적인 타입의 children만 받을 수 있을까?


children 여러개인 경우?

우선 동작방식을 이해해보자.

<Typography>
  나는
  <Typography fontWeight={"bold"}>
    두꺼운
  </Typography>
  글씨
</Typography>

위의 컴포넌트의 값들은 children으로 전달된다.
위처럼 전달되는 children들은 어떤 방식으로 전달될까?

쉽게 말하자면 아래와 같이 전달된다.

children: ['나는', <Typo/>, '글씨']
interface TypographyProps {
  children: string;
}

const Typography = ({children}: TypographyProps) => {
  //...codes
}

이렇게 선언을 하면 이제 children은 string type만 받을 수 있다.
조금만 응용하면 우리가 원하는 type의 children만 받을 수 있다는 것이다.


Typography 컴포넌트 허용하기

Typography의 컴포넌트 타입은 무엇일까

ReactElement<typeof Typography>

ReactElement만 선언한다면, 사실상 무의미하기에 원하는 컴포넌트의 타입만 추가해주도록 한다. 여러개의 컴포넌트가 들어올 수 있으니 배열 형태를 유지하자.

import { ReactElement } from 'react';

interface TypographyProps {
  children: (string | ReactElement<typeof Typography>)[];
}

const Typography = ({children}: TypographyProps) => {
  //...codes
}

요로코롬 간단하게 해결이 가능....😉👍..?
이라고 생각했는데.....

생각치 못한 문제가 발생했다.

예상과 달리 children으로 받는 것이 Typo 인스턴스 뿐 아니라 any를 받고있었기 때문이다... 😨


구조를 까보자.

interface ReactElement<P = any, T extends string | JSXElementConstructor<any> = string | JSXElementConstructor<any>> {
  type: T;
  props: P;
  key: Key | null;
}

앗... 역시 미리 까보는게 좋았었나...
여태까지 시도한 것들이... interface를 확인했다면 안해도 되었던 것을...

진짜 여러가지 방법으로 시도했으나 실패. 그 중 몇가지만 올려둔다.

Exclude(실패)

import React, { ReactElement } from 'react';

type TypographyType = React.FC<TypographyProps>;

const Typography = (({ children }: TypographyProps) => {
  return <div>{children}</div>;
}) as TypographyType;

type ExcludeIntrinsicElements<T> = T extends ReactElement<any, infer T>
  ? T extends keyof JSX.IntrinsicElements
    ? never
    : T
  : T;

interface TypographyProps {
  children: ExcludeIntrinsicElements<ReactElement<any> | string>[];
}

export default function App() {
  return (
    <div className="App">
      <Typography>
        통과해야함
        <div>에러떠야함</div>
        통과해야함
        <Typography>통과해야함</Typography>
      </Typography>
    </div>
  );
}

정적 field 추가 후 타입 체크(실패)

import { ReactElement } from 'react';

type TypographyChild =
  | string
  | (ReactElement<typeof Typography> & { isTypography: true });

interface TypographyProps {
  children: TypographyChild | TypographyChild[];
}

const Typography = ({ children }: TypographyProps) => {
  return <span>{children}</span>;
};

Object.assign(Typography, { isTypography: true });

export default function App() {
  return (
    <div className="App">
      <Typography>
        통과해야함
        <div>에러떠야함</div> //Error
        통과해야함
        <Typography>통과해야함</Typography> //Error
      </Typography>
    </div>
  );
}

...등
여기 없지만 이거저거 엄청 많이 시도했다..😂

글을 쓰는 지금 생각해보면 타입 캐스팅 마냥 interface 오버라이딩 해보는 것도 가능하지 않을까 생각이 들지만, 퇴근하고 자기 전까지 이것만 고민했기 때문에 패스하도록 한다..

결국은 런타임..

마침 코드 짜고있다가 다른 선생님이 오셔서 MUI의 사례를 보여주셨다.

갓갓

import React, { ReactNode, Children } from 'react';

const Typography: React.FC<TypographyProps> = ({ children }) => {
  Children.forEach(children, (child) => {
    if (typeof child !== 'string' && child?.type !== Typography) {
      throw new Error('에러');
    }
  });

  return <div>{children}</div>;
};

interface TypographyProps {
  children: ReactNode;
}

export default function App() {
  return (
    <div className="App">
      <Typography>
        통과해야함
        <div>에러떠야함</div>
        통과해야함
        <Typography>통과해야함</Typography>
      </Typography>
    </div>
  );
}

물론, 이미 런타임에서 에러를 잡는 방식의 코드를 고려해보지 않은 것은 아니지만 tsc단계에서 잡아내고싶어 계속 삽질을 했었던 것이지만...

MUI가 런타임에서 에러를 잡는다하면, 저 방식이 이상의 방식을 도출하는 것이 쉽지 않을 것이라 판단하고 여기서 패스!

0개의 댓글