管理具有交叉类型的数组并将其缩小范围的正确方法

Right way to manage Arrays with intersecting types and narrow them back down

在打字稿中使用具有不同类型的数组时,我倾向于 运行 遇到并非所有类型都存在的属性问题。

我运行针对页面上不同类型的部分、具有不同属性的用户的不同角色等等,进入同一个问题。

这里有一个动物的例子:

例如,如果您有猫、狗和狼类型:

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) {

}

如上评论,属性requiresWalks不能用来缩小类型

再假设我们有 20 只动物。我在实现可能扩展动物的类型时遇到困难,例如“AnimalThatRequiresWalks”,它可能具有与行走动物相关的多个属性。

将这些类型与类型“AnimalThatRequiresWalks”(具有属性“requiresWalks true”和“walkDistancePerDayKm”)连接起来的干净实现是什么?我如何才能正确地将其缩小到“AnimalThatRequiresWalks”?

您有两个问题:

  1. 如何检查requiresWalks动物是否需要散步?

  2. 如何在goForAWalkWith中定义animal的类型?

回复 #1:在尝试使用对象之前先测试对象是否具有 属性(这是缩小手册调用 in operator narrowing 的特定类型):

animals.forEach(animal => {
    if ("requiresWalks" in animal && animal.requiresWalks) {
// −−−−−^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
        goForAWalkWith(animal)
    }
});

Re #2,您可以从 Animal 联合中提取所有可通过 Extract utility type:

分配给 {requiresWalks: true} 的类型
function goForAWalkWith(animal: Extract<Animal, {requiresWalks: true}>) {
// −−−−−−−−−−−−−−−−−−−−−−−−−−−−−^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
    // ...
}

Extract<Animal, {requiresWalks: true}>Dog | Wolf.

Playground link

如果你不想这样做,你不必内联,你可以为它定义一个类型别名,然后使用别名:

type AnimalThatRequiresWalks = Extract<Animal, {requiresWalks: true}>;
// ...
function goForAWalkWith(animal: AnimalThatRequiresWalks) {
    // ...
}

Playground link


在评论中,您曾说过您希望将所有 AnimalThatRequiresWalks 属性一起定义为一个显式类型,而不是从 Dog、[=26] 推断该类型=],等等。你可以这样做:

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);
}

Playground link

您可能想尝试一下,看看开发人员的体验如何。 真的很容易忘记在定义DogWolf等时把那个AnimalThatRequiresWalks &部分放在上面,[=30=的类型] 是 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);
}

Playground link

也许这样更好,或者 over-engineered。 :-D