Typescript如何使用稍后指定的通用类型? [英] Typescript how to use a generic type which is specified later on?

查看:48
本文介绍了Typescript如何使用稍后指定的通用类型?的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

首先:这是我第一次使用CodeSandbox创建简化示例.欢迎提出任何改进建议!

First of all: This is the first time I made a CodeSandbox to create a simplified example. Any suggestions on how to improve this are welcome!

问题:
我想介绍动物的事实.有些事实在所有动物中共有,而另一些则是特定于动物的.在我的主要组件 App 中,我还不知道类型.因此,我想将其保留在通用的 Animal 级别上.我的主要组件发生了一些魔术(几乎只是一个API调用),现在我知道了类型.这使我渲染了一个特定的 Animal 组件.这些特定的成分本身具有更一般的成分,当然还有一些特定的动物数据.
现在,我只是无法思考如何使用打字稿正确地做到这一点.codesandbox应该可以解决问题:如您所见,编译器给了我很大的麻烦,因为 Animal 类型是未知的.是的.我该如何解决?我仍在学习打字稿,因此,如果我的一般做法不明智,那么我很乐意就如何构造它提供建议.

The issue:
I want to present animal facts. Some facts are shared among all animals, while others are animal specific. In my main component App, I do not know the type yet. So I want to keep it on the generic Animal level. Some magic happens (pretty much just an API call) in my main component and now I know the type. This yields in me rendering a specific Animal component. These specific components have more general components on their own, and, of course, some specific animal data.
Now I just cannot wrap my head around on how to do this properly with typescript. The codesandbox should clear things up: As you can see, the compiler is giving me a hard time, because the type Animal is unknown. And right so. How do I solve this? I am still learning typescript, so if my general approach to this is unwise, I am happy for suggestions on how else to structure this.

通过代码沙箱使其更易于理解

对于那些喜欢这里的代码类型的人来说,这里是:

And for those who prefer the code type here, here it is:

通用应用程序:

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;
};

推荐答案

一旦有了数据,就可以使用 in 运算符是内置的字体保护器.如果存在属性"feelsLikeaGod",则在动物中,那么我们有一个 TCat ,依此类推.

Once you have the data you can use type guards to figure out what type you have by examining its properties. The in operator is a built-in typeguard. If there is a property "feelsLikeaGod" in animal then we have a TCat and so on.

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游乐场链接

不幸的是,当我们在诸如 TFact< TCat |上的 data.animalSpecificDetails 之类的嵌套对象上检查属性时,这种方法不能很好地工作.TDog |TDuck> .

Unfortunately this doesn't work as nicely when we are checking a property on a nested object like data.animalSpecificDetails on TFact<TCat | TDog | TDuck>.

之所以有效,是因为因为打字稿会优化 data.animalSpecificDetails 的类型:

This works because because typescript will refine the type for data.animalSpecificDetails:

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

但是这不起作用,因为打字稿不会将这些优化应用于父对象:

But this doesn't work because typescript does not apply those refinements to the parent object:

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

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

We could use an assertion to keep things concise:

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

但是您需要注意,您所做的任何断言都是正确的.您可以通过做出无法保证的断言来应对运行时错误.

But you need to be careful that any assertions that you make are correct. You open yourself up to run-time errors by making assertions that aren't guaranteed.

您可以定义自己的用户-定义的类型警卫来检查父 TData 对象.

You could define your own user-defined type guards to check the parent TData object.

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

By the way, @Dom's comment is totally correct that Animal does not exist as a type outside the scope of the TFact. Animal is just the name for that type's T. You should define Animal either as the base object {randomFact1: string; randomFact2: string;} or as the union TCat | TDog | TDuck. My personal preference is for the base object.

// 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;
};

所以我们的类型防护看起来像这样.对于任何 Animal ,我们都有一个 TFact ,我们说如果这是真的,那么它是 TCat TFact

So our type guard would look like this. We have a TFact for any Animal and we say that if this is true, it's a TFact for a TCat.

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

因此,缺少处理此问题的方法.我确实想给您一些有效的代码,所以这是一种方法.我正在检查命名为 animal data.animalSpecificDetails 对象的属性,并将受保护的 animal 值与其余值组合 data 对象的

So there's lost of ways to handle this. I do want to give you some working code so here is one way. I am checking the properties on the data.animalSpecificDetails object, which I have named animal, and I am combining the guarded animal value with the rest of the data object.


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如何使用稍后指定的通用类型?的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆