【问题标题】:How can I write a react component that has dependent props in TypeScript?如何在 TypeScript 中编写具有依赖道具的反应组件?
【发布时间】:2021-03-08 04:20:01
【问题描述】:

如何为其中一个 props 的值取决于另一个 props 的值的 React 组件编写 Typescript 类型?

我正在尝试编写一个Icon 组件,该组件在 SVG 路径中查找预定义地图中的图标。地图是嵌套的,因此顶级属性是图标样式。这些样式中的每一个都有自己的地图,其中属性是图标名称。像这样:

const icons = {
  regular: {
    check: '',
  },
  solid: {
    on: '',
  },
};

组件接受一个 prop $flair 是图标样式和 $name 这是图标名称。我需要将这两个道具关联起来,以便$name 的值仅限于此$flair 的图标。如果$flair'regular',那么$name 可以是'check',但不能是'on'

到目前为止,这是我要做的:

type Icons = typeof icons;
type FlairNames = keyof Icons;
type IconNames = keyof Icons['regular'] | keyof Icons['solid'];

type Test = Extract<IconNames, keyof Icons['regular']>;

interface IconProps<F extends FlairNames> {
  $size?: number;
  $flair: F;
  $name: Extract<IconNames, keyof Icons[F]>;
}

function IconImpl({
  $size,
  $flair,
  $name,
}: IconProps<'regular'> | IconProps<'solid'>): JSX.Element {
  const flair = icons[$flair];
  const d = flair[$name];

  return (
    <svg>
      <path d={d} />
    </svg>
  );
}

Typescript Playground Link

我希望能够按如下方式使用该组件:

<Icon $flair="regular" $name="check" />
<Icon $flair="solid" $name="on" />
<Icon $flair="regular" $name="does-not-exist" /> // error
<Icon $flair="regular" /> // error
<Icon $name="on" /> // error

【问题讨论】:

  • 我可以帮助你输入这个,但是道具名称中的 $ 的目的是什么?

标签: reactjs typescript


【解决方案1】:

有一个联合

当您有两个需要匹配的道具时,实际上(在我看来)最好使用所有有效对的并集而不是泛型。当您拥有 F extends FlairNames 时,F 的值可以是联合 ('regular' | 'solid'),因此我们实际上无法保证匹配。

您可以使用映射类型计算这些匹配对:

type PossiblePairings = {
  // for each flair type (regular/solid)
  [FlairName in keyof Icons]: {
    // expect the prop flair to be this flair name
    $flair: FlairName,
    // and the prop name to be one of the keys of this flair object
    $name: keyof Icons[FlairName];
  }
  // access by keyof icon to flatten the map into a union
}[keyof Icons]

这解决了联合:

type PossiblePairings = {
    $flair: "regular";
    $name: "check";
} | {
    $flair: "solid";
    $name: "on";
}

所以现在我们将这个类型包含在IconProps 中,它不再是一个泛型。

type IconProps = {
  $size?: number;
} & PossiblePairings;

我们必须在实现中做出一个as 断言。 Typescript 不太明白我们的二级键始终是第一级键对象的属性。所以我们在访问flair[$name]时会出错:

元素隐式具有“任何”类型,因为表达式类型为“检查”| "on"' 不能用于索引类型 '{ check: string; } | { 上:字符串; }'。

类型'{ check: string; 上不存在属性'check' } | { 上:字符串; }'

function Icon({
  $size,
  $flair,
  $name,
}: IconProps): JSX.Element {
  const flair = icons[$flair];
  const d = (flair as Record<string, string>)[$name];

  return (
    <svg>
      <path d={d} />
    </svg>
  );
}

这适用于您的所有测试用例:

<Icon $flair="regular" $name="check" />
<Icon $flair="solid" $name="on" />

<Icon $flair="regular" $name="on" /> // error
<Icon $flair="solid" $name="check" /> // error

<Icon $flair="regular" $name="does-not-exist" /> // error
<Icon $flair="regular" /> // error
<Icon $name="on" /> // error

Typescript Playground Link

使用泛型

您之前的代码已经有 90%,但可以进行一些改进:

  • flair[$name] 上的错误与联合类型相同。
  • Extract&lt;IconNames, keyof Icons[F]&gt;keyof Icons[F] 相同
  • 我们不希望在IconProps&lt;'regular'&gt; | IconProps&lt;'solid'&gt; 中输入所有的flair 类型,因此我们应该使组件成为一个依赖于flair 类型的通用组件,并说props 是IconProps&lt;F&gt;
  • 进行这些更改后,flair[$name] 上的错误消失了,但现在&lt;path d={d} /&gt; 上出现了错误

这个修订版应该可以工作:

type FlairNames = keyof Icons;

interface IconProps<F extends FlairNames> {
  $size?: number;
  $flair: F;
  $name: keyof Icons[F];
}

function Icon<F extends FlairNames>({
  $size,
  $flair,
  $name,
}: IconProps<F>): JSX.Element {
  const flair = icons[$flair];
  const d = flair[$name] as unknown as string;

  return (
    <svg>
      <path d={d} />
    </svg>
  );
}

Typescript Playground Link

【讨论】:

  • 我喜欢工会的解决方案。它使非法状态无法表示
猜你喜欢
  • 2021-04-14
  • 2019-10-10
  • 2020-06-12
  • 1970-01-01
  • 1970-01-01
  • 2021-09-07
  • 2020-10-17
  • 2021-02-22
  • 2020-10-13
相关资源
最近更新 更多