包含不同类型的任一类型

Either type containing different types

[原始 post 编辑为简化示例]

我想创建一个组件,它将采用一对值(两个值基于相同的类型 -> valuesetValue),并且基于它,运行 将根据这些类型发挥作用。充分说明情况:

AnimalAndCatTypes.ts:

export interface Animal {
  type: string,
  age: number
}

export interface Cat extends Animal {
  typeOfCat: string,
}

ChangeSpecies.tsx:

import React from "react";
import { Animal, Cat } from "./AnimalAndCatTypes";

interface AnimalVersion {
  value: Animal;
  setValue: React.Dispatch<React.SetStateAction<Animal>>;
}

interface CatVersion {
  value: Cat;
  setValue: React.Dispatch<React.SetStateAction<Cat>>;
}

type Props = AnimalVersion | CatVersion;

const ChangeSpecies = (props: Props): JSX.Element => {
  const { value, setValue } = props;

  const handleChange = (data: string) => {
    setValue({ ...value, type: data });
  };

  return (
    <form>
      <label>
        <input
          type="text"
          value={value.type}
          onChange={(data) => handleChange(data.target.value)}
        />
      </label>
    </form>
  );
};

export default ChangeSpecies;

ExampleComponent.tsx:

import React, { useState } from "react";
import { Animal, Cat } from "./AnimalAndCatTypes";
import ChangeSpecies from "./ChangeSpecies";

const ExampleComponent = (): JSX.Element => {
  const [animal, setAnimal] = useState<Animal>({
    type: "dog",
    age: 2
  });
  const [cat, setCat] = useState<Cat>({
    type: "cat",
    age: 1,
    typeOfCat: "persian"
  });

  return (
    <div>
      <ChangeSpecies value={animal} setValue={setAnimal} />
      <ChangeSpecies value={cat} setValue={setCat} />
    </div>
  );
};

export default ExampleComponent;

问题出在 setValue,打字稿说 React.Dispatch<React.SetStateAction<Cat>> 中不能使用 Animal 因为缺少 typeOfCat.

如何创建一个类型,让 ts 在创建组件时通过检查 value 和 [=14= 知道 PropsCatAnimal ] 来自一对。稍后,根据那对类型,将采用链接到 value 类型且不能与其他类型混合的正确 setValue 类型?

[编辑中添加]

Link转载:https://codesandbox.io/s/zealous-mirzakhani-1x4kz?file=/src/App.js

主要问题在这个联合中:

type Props = AnimalVersion | CatVersion

因为Props是并集,所以setValue也是React.Dispatch<React.SetStateAction<Animal>> | React.Dispatch<React.SetStateAction<Cat>>的并集。

根据 docs:

Likewise, multiple candidates for the same type variable in contra-variant positions causes an intersection type to be inferred:

这意味着,如果你想调用这样的函数,联合中所有函数的参数将合并(相交),这就是为什么 setValue 期望 React.SetStateAction<Animal> & React.SetStateAction<Cat>.

请看简单的例子:

declare var foo: (val: { name: string }) => void
declare var bar: (val: { surname: number }) => void

declare var union:typeof foo | typeof bar

// expects { name: string;} & { surname: number; }
union()

大多数时候,人们收到 never,因为:

declare var foo: (val:  string) => void
declare var bar: (val:  number ) => void

declare var union:typeof foo | typeof bar

// expects never
union()

string & number -> 生成 never,因为这种类型不可表示。

在你的例子中,setValue 需要 Cat,因为 CatAnimal 的子类型。超类型 Animal 和子类型 Cat 的交集 - 给你 Cat 类型。你有一个错误,因为 setValue 期望 Catprops.value 可能是一个 Animal,没有额外的 typeOfCat 类型。

现在,当我们知道为什么会出现交叉点时,我们就可以继续了。

为了修复它,您可以为 Animal|Cat 创建额外的通用参数:

import React from "react";

export interface Animal {
  type: string,
  age: number
}

export interface Cat extends Animal {
  typeOfCat: string,
}


interface AnimalVersion {
  value: Animal;
  setValue: React.Dispatch<React.SetStateAction<Animal>>;
}

interface CatVersion {
  value: Cat;
  setValue: React.Dispatch<React.SetStateAction<Cat>>;
}


type Props<T> = {
  value: T,
  setValue: React.Dispatch<React.SetStateAction<T>>;
}


const ChangeSpecies = <T extends Animal | Cat,>(props: Props<T>): JSX.Element => {
  const { value, setValue } = props;

  const handleChange = (data: string) => {
    setValue({ ...value, type: data });
  };

  return (
    <form>
      <label>
        <input
          type="text"
          value={value.type}
          onChange={(data) => handleChange(data.target.value)}
        />
      </label>
    </form>
  );
};

const jsx = <ChangeSpecies value={{ type: 'animal', age: 42 }} setValue={(elem /** elem is animal */) => {

}} />


const jsx2 = <ChangeSpecies value={{ type: 'animal', age: 42, typeOfCat: 'cat' }} setValue={(elem /**elem is infered as a cat */) => {

}} />

// error
const expectedError = <ChangeSpecies value={{ type: 'animal', typeOfCat: 'cat' }} setValue={(elem ) => {

}} />

Playground

现在,TS 知道 value 具有 setValue 的有效类型。 SInce,两种类型都有type属性,这一行setValue({ ...value, type: data });有效

但是,您应该了解此解决方案的缺点:


type Props<T> = {
  value: T,
  setValue: React.Dispatch<React.SetStateAction<T>>;
}

type Result = Props<Animal | Cat>

const invalid: Result = {
  value: { type: 'animal', age: 42 },
  setValue: (elem /**React.SetStateAction<Animal | Cat> */) => {

  },
}

TS还是认为elem是联合

意思是如果你在组件构造时提供显式联合泛型:


const jsx = <ChangeSpecies<Animal|Cat> value={{ type: 'animal', age: 42 }} setValue={(elem) => {

}} />

TS 会让你做到。


第二个选项

更安全的解决方案是使用 distributive-conditional-types:

type Props<T> = T extends any ? {
  value: T,
  setValue: React.Dispatch<React.SetStateAction<T>>;
} : never

type Result = Props<Animal | Cat>

其实这个类型就等于你表示的Props union。它只是以更通用的方式编写的。

如果您想坚持使用此解决方案,则应创建 custom typeguard。这意味着它将影响您的运行时间。虽然,可以为值编写类型保护,如 isStringisNumer,但很难为函数编写类型保护。因为您只能通过引用或 arity.length 来比较函数。所以,我觉得,不值得。