管理具有交叉类型的数组并将其缩小范围的正确方法
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”?
您有两个问题:
如何检查requiresWalks
动物是否需要散步?
如何在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
.
如果你不想这样做,你不必内联,你可以为它定义一个类型别名,然后使用别名:
type AnimalThatRequiresWalks = Extract<Animal, {requiresWalks: true}>;
// ...
function goForAWalkWith(animal: AnimalThatRequiresWalks) {
// ...
}
在评论中,您曾说过您希望将所有 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);
}
您可能想尝试一下,看看开发人员的体验如何。 真的很容易忘记在定义Dog
、Wolf
等时把那个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);
}
也许这样更好,或者 over-engineered。 :-D
在打字稿中使用具有不同类型的数组时,我倾向于 运行 遇到并非所有类型都存在的属性问题。
我运行针对页面上不同类型的部分、具有不同属性的用户的不同角色等等,进入同一个问题。
这里有一个动物的例子:
例如,如果您有猫、狗和狼类型:
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”?
您有两个问题:
如何检查
requiresWalks
动物是否需要散步?如何在
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
.
如果你不想这样做,你不必内联,你可以为它定义一个类型别名,然后使用别名:
type AnimalThatRequiresWalks = Extract<Animal, {requiresWalks: true}>;
// ...
function goForAWalkWith(animal: AnimalThatRequiresWalks) {
// ...
}
在评论中,您曾说过您希望将所有 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);
}
您可能想尝试一下,看看开发人员的体验如何。 真的很容易忘记在定义Dog
、Wolf
等时把那个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);
}
也许这样更好,或者 over-engineered。 :-D