首先:这是我第一次制作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;
};
答案 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
}
}
不幸的是,当我们检查嵌套对象(如 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;
};
所以我们的类型保护看起来像这样。我们有任何 TFact
的 Animal
,我们说如果这是真的,它是 TFact
的 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>
);
}