Zenn
😁

TypeScriptにおけるコンポーネント間の引数型厳密比較について、解決策を調査する

2025/02/09に公開
1

はじめに

親コンポーネントから子コンポーネントへスプリット構文を利用して引数を渡すと、不要なプロパティを許容するので困っている。
解決策がないか調査する。

ESLintのReact/jsx-props-no-spreadingを有効にする。という選択肢は、一度脇に置いておきます。

https://github.com/jsx-eslint/eslint-plugin-react/blob/master/docs/rules/jsx-props-no-spreading.md
https://qiita.com/sabawo/items/0de5638236835ca652be

試しに

type.ts
export type Exact<T, U extends T = T> = T & {
  [K in keyof U]: K extends keyof T ? T[K] : never;
};

export type DataProps = {
  title: string;
  description: string;
  sercretInfo: string;
};

export type DataPropsWithCC = Exact<
  Omit<DataProps, "sercretInfo">
>;
RSC.tsx
import CC from "./CC";

import { DataProps } from "./type";

const getData = async () => {
  // 任意のデータ取得ロジック
  const data: DataProps = {
    title: "title",
    description: "description",
    sercretInfo: "sercretInfo",
  };

  return data;
};

const RSC = async () => {
  const data = await getData();
  const { sercretInfo, ...restData } = data;

  return (
    <>
      {/* 型エラーが起こらないでほしい */}
      <CC
        title={restData.title}
        description={restData.description}
      />

      {/* 型エラーが起こってほしい */}
      <CC {...data} />

      {/* 型エラーが起こらないでほしい */}
      <CC {...restData} />
    </>
  );
};

export default RSC;
CC.tsx
"use client";

import { DataPropsWithCC } from "./type";

const CC = (props: DataPropsWithCC) => {
  const { title, description } = props;
  return (
    <>
      <h1>{title}</h1>
      <p>{description}</p>
    </>
  );
};

export default CC;

{/* 型エラーが起こってほしい */}
<CC {...data} />

上記のように型上はsercretInfoが不要なのだが、スプリット構文で引数を渡すと型に許容されてしまう。これを改善したい。

そもそも原因は何か。

TypeScriptの型システムが構造的部分型を採用しているから

https://qiita.com/uhyo/items/b1f806531895cb2e7d9a
ということらしい。恥ずかしながら知らなかった。

解決策については
先人の知識があった。とてもありがたい。
https://zenn.dev/qsf/articles/6e346f7fd3aaf1

よって、最終的に以下となる。

type
export type StrictPropertyCheck<T, TExpected, TError> = T &
  (Exclude<keyof T, keyof TExpected> extends never
    ? {}
    : TError);

export type DataProps = {
  title: string;
  description: string;
  sercretInfo: string;
};

export type DataPropsWithCC = Omit<
  DataProps,
  "sercretInfo"
>;
RSC
import CC from "./CC";

import { DataProps } from "./type";

const getData = async () => {
  // 任意のデータ取得ロジック
  const data: DataProps = {
    title: "title",
    description: "description",
    sercretInfo: "sercretInfo",
  };

  return data;
};

const RSC = async () => {
  const data = await getData();
  const { sercretInfo, ...restData } = data;

  return (
    <>
      {/* 型エラーが起こらない */}
      <CC
        title={restData.title}
        description={restData.description}
      />

      {/* 型エラーが起こる */}
      <CC {...data} />

      {/* 型エラーが起こらない */}
      <CC {...restData} />
    </>
  );
};

export default RSC;
CC
"use client";

import {
  DataPropsWithCC,
  StrictPropertyCheck,
} from "./type";

const CC = <T extends DataPropsWithCC>(
  props: StrictPropertyCheck<
    T,
    DataPropsWithCC,
    `T has excess property`
  >
) => {
  const { title, description } = props;
  return (
    <>
      <h1>{title}</h1>
      <p>{description}</p>
    </>
  );
};

export default CC;

終わりに

とてもためになった。
そうか。そうだったのか。

参考文献

https://zenn.dev/qsf/articles/6e346f7fd3aaf1
https://qiita.com/uhyo/items/b1f806531895cb2e7d9a
https://zenn.dev/estra/articles/typescript-type-set-hierarchy#型の階層性

1

Discussion

ログインするとコメントできます