基于数组的*任何*元素(不是*所有*元素)的类型区分

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({ ... });

Playground