Typescript如何使用稍后指定的通用类型? [英] Typescript how to use a generic type which is specified later on?
问题描述
首先:这是我第一次使用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
}
}
不幸的是,当我们在诸如 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屋!