在打字稿中使用具有不同类型的数组时,我往往会遇到并非所有类型的属性问题。
对于页面上不同类型的部分,具有不同属性的用户的不同角色等等,我遇到了相同的问题。
下面是一个动物的例子:
例如,如果您有猫、狗和狼的类型:
export type Cat = {
animal: 'CAT';
legs: 4;
}
export type Dog = {
animal: 'DOG',
legs: 4,
requiresWalks: true,
walkDistancePerDayKm: 5
}
export type Wolf = {
animal: 'WOLF',
legs: 4,
requiresWalks: true,
walkDistancePerDayKm: 20
}
type Animal = Cat | Dog | Wolf;
const animals: Animal[] = getAnimals();
animals.forEach(animal => {
// here i want to check if the animal requires a walk
if (animal.requiresWalks) {
// Property 'requiresWalks' does not exist on type 'Animal'. Property 'requiresWalks' does not exist on type 'Cat'.
goForAWalkWith(animal)
}
});
// The type "AnimalThatRequiresWalks" does not exist and i want to know how to implement it
goForAWalkWith(animal: AnimalThatRequiresWalks) {
}
如上所述,属性要求步行不能用于缩小类型。
再想象一下我们有20只动物。我在实现可能扩展动物的类型时遇到困难,例如"AnimalThatRequiresWalks",这些类型可能具有与行走动物相关的多个属性。
将这些类型与类型"AnimalThatRequiresWalks"(具有属性"requiresWalks true"和"walkDistancePerDayKm")连接起来的干净实现是什么,我怎样才能正确地将其缩小到"AnimalThatRequiresWalks"?
您有两个问题:
-
你如何检查
requiresWalks
以查看动物是否需要散步? -
您如何定义
goForAWalkWith
中animal
的类型?
Re #1:在尝试使用它之前,您测试以查看对象是否具有该属性(这是一种特定类型的缩小手册调用in
运算符缩小):
animals.forEach(animal => {
if ("requiresWalks" in animal && animal.requiresWalks) {
// −−−−−^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
goForAWalkWith(animal)
}
});
Re #2,您可以从Animal
联合中提取可通过Extract
实用程序类型分配给{requiresWalks: true}
的所有类型:
function goForAWalkWith(animal: Extract<Animal, {requiresWalks: true}>) {
// −−−−−−−−−−−−−−−−−−−−−−−−−−−−−^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
// ...
}
Extract<Animal, {requiresWalks: true}>
是Dog | Wolf
.
游乐场链接
如果您不想这样做,则不必内联执行此操作,您可以为其定义类型别名,然后使用该别名:
type AnimalThatRequiresWalks = Extract<Animal, {requiresWalks: true}>;
// ...
function goForAWalkWith(animal: AnimalThatRequiresWalks) {
// ...
}
游乐场链接
在注释中,您说过要用显式类型一起定义所有AnimalThatRequiresWalks
属性,而不是从Dog
、Cat
等推断该类型。你可以这样做:
interface AnimalThatRequiresWalks {
animal: string;
requiresWalks: true;
preferredWalkTerrain: "hills" | "paths" | "woods";
walkDistancePerDayKm: number;
}
export type Cat = {
animal: "CAT";
legs: 4;
};
export type Dog = AnimalThatRequiresWalks & {
animal: "DOG";
legs: 4;
walkDistancePerDayKm: 5; // Only needed if you want to refine its type
preferredWalkTerrain: "paths"; // Same
};
export type Wolf = AnimalThatRequiresWalks & {
animal: "WOLF";
legs: 4;
walkDistancePerDayKm: 20; // Only needed if you want to refine its type
preferredWalkTerrain: "woods"; // Same
}
type Animal = Cat | Dog | Wolf;
declare const animals: Animal[]; // = getAnimals();
animals.forEach(animal => {
if ("requiresWalks" in animal && animal.requiresWalks) {
goForAWalkWith(animal)
}
});
function goForAWalkWith(animal: AnimalThatRequiresWalks) {
console.log("Go for a walk with the " + animal.animal);
}
游乐场链接
您可能想尝试一下,看看开发人员的体验是什么样的。在定义Dog
、Wolf
等时,很容易忘记把AnimalThatRequiresWalks &
部分放在上面,AnimalThatRequiresWalks["animal"]
的类型是string
而当你推断它是更窄的"Cat" | "Dog" | "Wolf"
。但它可能更方便。
碰巧的是,解决一开始容易忘记AnimalThatRequiresWalks &
的一种方法是对动物使用泛型类型:
interface AnimalThatRequiresWalks {
animal: string;
requiresWalks: true;
preferredWalkTerrain: "hills" | "paths" | "woods";
walkDistancePerDayKm: number;
}
type AnimalType<R extends boolean, Type extends object> =
R extends true
? AnimalThatRequiresWalks & Type
: Type;
export type Cat = AnimalType<false, {
animal: "CAT";
legs: 4;
}>;
export type Dog = AnimalType<true, {
animal: "DOG";
legs: 4;
walkDistancePerDayKm: 5; // Only needed if you want to refine its type
preferredWalkTerrain: "paths"; // Same
}>;
export type Wolf = AnimalType<true, {
animal: "WOLF";
legs: 4;
walkDistancePerDayKm: 20; // Only needed if you want to refine its type
preferredWalkTerrain: "woods"; // Same
}>;
type Animal = Cat | Dog | Wolf;
declare const animals: Animal[]; // = getAnimals();
animals.forEach(animal => {
if ("requiresWalks" in animal && animal.requiresWalks) {
goForAWalkWith(animal)
}
});
function goForAWalkWith(animal: AnimalThatRequiresWalks) {
console.log("Go for a walk with the " + animal.animal);
}
游乐场链接
也许这样更好,或者它被过度设计了。 :-D