typescript如何使用稍后指定的泛型类型?

IT技术 reactjs typescript generics typescript-generics
2021-05-20 19:46:37

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

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

Codesandbox 使其更易于理解

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

一般应用程序:

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个回答

获得数据后,您可以使用类型保护通过检查其属性来确定您拥有的类型。in运营商是一个内置的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游乐场链接

不幸的是,当我们检查像data.animalSpecificDetailson这样的嵌套对象的属性时,这并不能很好地工作TFact<TCat | TDog | TDuck>

这是有效的,因为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>} />
}

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

您可以定义自己的用户定义类型保护来检查父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;
};

所以我们的类型保护看起来像这样。我们有 a TFactfor anyAnimal并且我们说如果这是真的,那就是 a TFactfor a TCat

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