【问题标题】:Typescript how to use a generic type which is specified later on?Typescript 如何使用稍后指定的泛型类型?
【发布时间】:2021-06-03 12:59:48
【问题描述】:

首先:这是我第一次做 CodeSandbox 来创建一个简化的例子。欢迎就如何改进这一点提出任何建议!

问题:
我想介绍动物事实。有些事实是所有动物共有的,而另一些则是特定于动物的。在我的主要组件App 中,我还不知道类型。所以我想把它保持在通用的Animal 级别。在我的主要组件中发生了一些魔法(几乎只是一个 API 调用),现在我知道了类型。这会在我渲染一个特定的Animal 组件。这些特定组件本身具有更通用的组件,当然还有一些特定的动物数据。
现在,我无法完全理解如何使用 typescript 正确执行此操作。代码框应该把事情弄清楚:正如你所看到的,编译器让我很难过,因为类型 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;
};

【问题讨论】:

  • 从您的示例中,看起来 Animal 并未实际定义。当Animal 用作泛型时,我看到的唯一实例,即Animal extends TCat | TDog | TDuck。在这种情况下,没有实际定义,因为 Animal 被视为变量 (T)。这意味着它仅与该特定定义相关。您需要定义它(类型、接口或类)并将其导出以供其他文件使用。

标签: reactjs typescript generics typescript-generics


【解决方案1】:

获得数据后,您可以使用type guards 通过检查其属性来确定您拥有的类型。 in operator 是一个内置的类型保护。如果有一个属性"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&lt;TCat | TDog | TDuck&gt; 上的属性时)效果不佳。

这是可行的,因为 typescript 将优化 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;
};

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

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

因此,我们已经失去了处理这个问题的方法。我确实想给你一些工作代码,所以这是一种方法。我正在检查 data.animalSpecificDetails 对象的属性,我将其命名为 animal,并将受保护的 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

【讨论】:

  • 谢谢你。我认为我的整体问题措辞不佳。我想要实现的是,在我的大多数组件中,我可以只使用data: TFact&lt;Animal&gt;,然后使用data.randomFact1 访问它,而不是data.animalSpecific.randomFact1。所以我想我正在寻找的是构建一个适用于所有人的通用组件,它接受一个泛型,上面写着“我可以是任何子类,没关系”,并且打字稿确保我只使用与类型无关的属性在这些组件中
  • 你已经为这个问题提供了一个解决方案,谢谢你 :) 我为这个评论开了一张新票,看这里stackoverflow.com/questions/66489072/…如果你有兴趣
猜你喜欢
  • 1970-01-01
  • 2021-06-18
  • 2020-11-11
  • 1970-01-01
  • 2019-05-30
  • 1970-01-01
  • 2019-07-23
  • 2021-12-06
  • 2020-01-10
相关资源
最近更新 更多