打字稿如何使用稍后指定的泛型类型?

时间:2021-03-04 20:24:38

标签: reactjs typescript generics typescript-generics

首先:这是我第一次制作CodeSandbox来创建一个简化的例子。欢迎提出有关如何改进此问题的任何建议!

问题:
我想展示动物的事实。一些事实是所有动物共有的,而另一些则是特定于动物的。在我的主要组件 App 中,我还不知道类型。所以我想把它保持在通用的 Animal 级别。在我的主要组件中发生了一些魔法(几乎只是一个 API 调用),现在我知道类型了。这会导致我呈现特定的 Animal 组件。这些特定的组件本身就有更多的通用组件,当然还有一些特定的动物数据。
现在我无法思考如何使用打字稿正确地做到这一点。代码和框应该清除问题:如您所见,编译器给我带来了困难,因为类型 Animal 是未知的。没错。我该如何解决这个问题?我仍在学习打字稿,所以如果我对此的一般方法是不明智的,我很乐意提供有关如何构建它的建议。

Codesandbox to make it more understandable

对于喜欢这里代码类型的人,这里是:

通用应用:

export default function App() {
  const [data, setData] = React.useState<TFact<Animal>>();

  return (
    <div className="App">
      <h1>Hi there</h1>
      {/* This is part of a switch case, I know at this point
      what kind of animal to render */}
      <Cat data={data} />
      {/*<Dog data={data} />*/}
    </div>
  );
}

子组件的两个例子:

interface IProps {
  data: TFact<TCat>;
}

const Cat = ({ data }: IProps) => {
  return (
    <div>
      <GeneralChild data={data} />
      Meow!
    </div>
  );
};

export default Cat;

第二个:

interface IProps {
  data: TFact<TDog>;
}

const Dog = ({ data }: IProps) => {
  return (
    <div>
      <GeneralChild data={data} />
      Woof!
    </div>
  );
};

export default Dog;

一般儿童:

interface IProps {
  data: TFact<Animal>;
}

const GeneralChild = ({ data: IProps }) => {
  return (
    <div>
      Well I can be anything! And that is okay, because I only need the data
      shared by all components!
    </div>
  );
};

export default GeneralChild;

最相关的是打字:

export type TFact<Animal extends TCat | TDog | TDuck> = {
  name: string;
  age: number;
  animalSpecificDetails: Animal;
};

export type TCat = {
  randomFact1: string;
  randomFact2: string;
  feelsLikeaGod: boolean;
};

export type TDog = {
  randomFact1: string;
  randomFact2: string;
  alwaysLoyal: boolean;
};

export type TDuck = {
  randomFact1: string;
  randomFact2: string;
  sound: string;
};

1 个答案:

答案 0 :(得分:1)

获得数据后,您可以使用 type guards 通过检查其属性来确定您拥有的类型。 in operator 是一个内置的 typeguard。如果有一个属性 "feelsLikeaGod" in animal 那么我们有一个 TCat 等等。

function doCatThing(cat: TCat) { }
function doDogThing(dog: TDog) { }
function doDuckThing(duck: TDuck) { }

function myFunc(animal: TCat | TDog | TDuck) {
    if ("feelsLikeaGod" in animal) {
        doCatThing(animal);  // animal has type TCat
    } else if ("alwaysLoyal" in animal) {
        doDogThing(animal); // animal has type TDog
    } else {
        // don't even need to check the last case because TDuck is the only possibility
        doDuckThing(animal); // animal has type TDuck
    }
}

TypeScript Playground Link

不幸的是,当我们检查嵌套对象(如 data.animalSpecificDetails 上的 TFact<TCat | TDog | TDuck>)的属性时,这不能很好地工作。

这是可行的,因为打字稿会优化 data.animalSpecificDetails 的类型:

if ("alwaysLoyal" in data.animalSpecificDetails) {
  return <Dog data={{...data, animalSpecificDetails: data.animalSpecificDetails}}/>
}

但这不起作用,因为 typescript 不会将这些改进应用于父对象:

if ("alwaysLoyal" in data.animalSpecificDetails) {
  return <Dog data={data}/>
}

我们可以使用断言来保持简洁:

if ("feelsLikeaGod" in data.animalSpecificDetails) {
  return <Cat data={data as TFact<TCat>} />
}

但是您需要注意您所做的任何断言都是正确的。通过做出无法保证的断言,您会面临运行时错误。

您可以定义自己的 user-defined type guards 来检查父 TData 对象。

顺便说一句,@Dom 的评论是完全正确的,即 Animal 不作为 TFact 范围之外的类型存在。 Animal 只是该类型的 T 的名称。您应该将 Animal 定义为基础对象 {randomFact1: string; randomFact2: string;} 或联合 TCat | TDog | TDuck。我个人更喜欢基础对象。

// All animals have these properties
type Animal = {
    randomFact1: string;
    randomFact2: string;
}

// T is the generic variable for this type
// You can call the variable anything, but I changed it to T to be clearer about what it is
export type TFact<T extends Animal> = {
    name: string;
    age: number;
    animalSpecificDetails: T;
};

// We don't need to repeat as much because we can use Animal
export type TCat = Animal & {
    feelsLikeaGod: boolean;
};

所以我们的类型保护看起来像这样。我们有任何 TFactAnimal,我们说如果这是真的,它是 TFactTCat

const isCatFact = (fact: TFact<Animal>): fact is TFact<TCat> => {
    return "feelsLikeaGod" in fact.animalSpecificDetails;
}

所以有很多方法可以处理这个问题。我确实想给你一些工作代码,所以这是一种方法。我正在检查名为 data.animalSpecificDetailsanimal 对象的属性,并将受保护的 animal 值与 data 对象的其余部分结合起来。


function App() {
    const [data, setData] = React.useState<TFact<TCat | TDog | TDuck>>();

    const renderAnimal = () => {
        // skip undefined
        if (!data) {
            return;
        }
        //just so we don't have to write data.animalSpecificDetails every time
        const animal = data.animalSpecificDetails;
        // cat
        if ("feelsLikeaGod" in animal) {
            return <Cat data={{ ...data, animalSpecificDetails: animal }} />
        }
        // dog
        else if ("alwaysLoyal" in animal) {
            return <Dog data={{ ...data, animalSpecificDetails: animal }} />
        }
        // duck
        else {
            return <Duck data={{ ...data, animalSpecificDetails: animal }} />
        }
    }

    return (
        <div className="App">
            <h1>Hi there</h1>
            {renderAnimal()}
        </div>
    );
}

Typescript Playground Link