基于数组的*任何*元素(不是*所有*元素)的类型区分
Type discrimination based on *any* element of an array (not *all* elements)
我想在 TypeScript 中创建一个联合类型,它可以使用数组作为判别式,但如果数组的 any 元素满足某些条件,则具有特定类型匹配,而不是数组中的 all 个元素。
例如,这应该可行,因为任何养狗的家庭都可以指定品种:
const homes: Household[] = [{
humans: 3,
pets: ['cat', 'dog'],
dogBreeds: ['mutt'],
}];
但这不应该,因为没有狗的家庭不应该指定狗的品种:
const homes: Household[] = [{
humans: 3,
pets: ['cat'],
dogBreeds: ['mutt'],
}];
我的想法是我应该这样定义我的类型:
type PetType = 'dog' | 'cat' | 'hamster';
interface NoDogHousehold {
humans: number;
pets: Exclude<PetType, 'dog'>[];
dogBreeds?: never;
}
interface DogHousehold {
humans: number;
pets: 'dog'[];
dogBreeds: string[];
}
type Household = DogHousehold | NoDogHousehold;
问题在于 DogHousehold
现在仅在 pets
中的 所有 元素为 'dog' 时才适用。所以它适用于 ['dog']
或 ['dog', 'dog']
,但不适用于 ['cat', 'dog']
。使用元组类型可以工作,但我看不出有什么方法可以在没有可预测的 number/order 元素的情况下创建这样的类型。我能想到的最接近的元组类型是 ['dog', PetType?, PetType?]
,但这很尴尬,只有当狗先出现时才有效。
有什么方法可以让 TypeScript 像这样强制执行类型正确性吗?或者这种类型歧视是不可能的?
为此,我们需要一种方法让 dogBreeds
知道 pets
是什么。为此,我们需要一个泛型:
type Household<Pets extends ReadonlyArray<string>> = {
当然 pets
是类型 Pets
:
pets: Pets;
}
那么我们说,如果Pets
包括"dog"
,加上dogBreeds
:
& (Pets extends [] ? {} : "dog" extends Pets[number] ? { dogBreeds: string[]; } : {})
但首先我们检查 Pets
是否为空。否则如果它是空的,dogBreeds
无论如何都会出现。
我们将此检查的结果与 { pets: Pets; }
的基数相交。
那我们也可以用猫来做:
& (Pets extends [] ? {} : "cat" extends Pets[number] ? { catBreeds: string[]; } : {})
但是,我们不能像这样只使用这种类型:
const house: Household = { ... };
TypeScript 要求我们在这里使用泛型,但随后我们需要复制代码,这并不理想。
为了解决这个问题,我们需要一个包装函数来为我们进行推断:
function household<Pets extends ReadonlyArray<string>>(household: Household<Pets>): Household<Pets> {
return household;
}
现在我们可以使用它了:
const house = household({ ... });
我想在 TypeScript 中创建一个联合类型,它可以使用数组作为判别式,但如果数组的 any 元素满足某些条件,则具有特定类型匹配,而不是数组中的 all 个元素。
例如,这应该可行,因为任何养狗的家庭都可以指定品种:
const homes: Household[] = [{
humans: 3,
pets: ['cat', 'dog'],
dogBreeds: ['mutt'],
}];
但这不应该,因为没有狗的家庭不应该指定狗的品种:
const homes: Household[] = [{
humans: 3,
pets: ['cat'],
dogBreeds: ['mutt'],
}];
我的想法是我应该这样定义我的类型:
type PetType = 'dog' | 'cat' | 'hamster';
interface NoDogHousehold {
humans: number;
pets: Exclude<PetType, 'dog'>[];
dogBreeds?: never;
}
interface DogHousehold {
humans: number;
pets: 'dog'[];
dogBreeds: string[];
}
type Household = DogHousehold | NoDogHousehold;
问题在于 DogHousehold
现在仅在 pets
中的 所有 元素为 'dog' 时才适用。所以它适用于 ['dog']
或 ['dog', 'dog']
,但不适用于 ['cat', 'dog']
。使用元组类型可以工作,但我看不出有什么方法可以在没有可预测的 number/order 元素的情况下创建这样的类型。我能想到的最接近的元组类型是 ['dog', PetType?, PetType?]
,但这很尴尬,只有当狗先出现时才有效。
有什么方法可以让 TypeScript 像这样强制执行类型正确性吗?或者这种类型歧视是不可能的?
为此,我们需要一种方法让 dogBreeds
知道 pets
是什么。为此,我们需要一个泛型:
type Household<Pets extends ReadonlyArray<string>> = {
当然 pets
是类型 Pets
:
pets: Pets;
}
那么我们说,如果Pets
包括"dog"
,加上dogBreeds
:
& (Pets extends [] ? {} : "dog" extends Pets[number] ? { dogBreeds: string[]; } : {})
但首先我们检查 Pets
是否为空。否则如果它是空的,dogBreeds
无论如何都会出现。
我们将此检查的结果与 { pets: Pets; }
的基数相交。
那我们也可以用猫来做:
& (Pets extends [] ? {} : "cat" extends Pets[number] ? { catBreeds: string[]; } : {})
但是,我们不能像这样只使用这种类型:
const house: Household = { ... };
TypeScript 要求我们在这里使用泛型,但随后我们需要复制代码,这并不理想。
为了解决这个问题,我们需要一个包装函数来为我们进行推断:
function household<Pets extends ReadonlyArray<string>>(household: Household<Pets>): Household<Pets> {
return household;
}
现在我们可以使用它了:
const house = household({ ... });