TypeScript:Select 个数组元素 属性

TypeScript: Select element of an array by type property

在 TypeScript 项目中,我有一个容器数组,其中包含 type 属性 和一些附加数据,具体取决于它们的类型。

type Container<Type extends string> = {
    type: Type;
}

type AContainer = Container<"a"> & {
    dataA: number;
}

type BContainer = Container<"b"> & {
    dataB: boolean;
}

const data: (AContainer | BContainer)[] = [
    { type: "a", dataA: 17 },
    { type: "b", dataB: true }
];

我的目标是编写一个函数,允许我通过 type 从该数组中 select 一个元素,具有完整的类型安全性。像这样:

const getByType = <T extends string>(data: Container<string>[], type: T): Container<T> => {
    for (const c of data) {
        if (c.type === type) return c;
    }
    throw new Error(`No element of type ${type} found.`);
};

const dataA: AContainer = getByType(data, "a");

问题在于试图让 TypeScript 相信该函数是类型安全的,并且 return 值是原始数组的一个元素并且具有请求的类型。

这是我的最佳尝试:

const getByType = <ContainerType extends Container<string>, Type extends string>(data: (ContainerType & Container<string>)[], type: Type): ContainerType & Container<Type> => {
    for (const c of data) {
        if (c.type === type) return c;
    }
    throw new Error(`No element of type ${type} found.`);
};

然而,TypeScript 既不理解比较 c.type === type 确保 Container<string> 变成 Container<Type>,也不理解示例调用的 return 类型,[=由于 Container<"b"> & Container<"a"> 中的冲突,21=] 等于 AContainer。 第一个问题可以通过在下面的代码块中使用类型谓词来解决(虽然那种感觉有点像作弊),但我还没有找到第二个问题的解决方案。

const isContainer = <Type extends string>(c: Container<string>, type: Type): c is Container<Type> => {
    return typeof c === "object" && c.type === type;
};

有什么方法可以让它发挥作用吗?如果 getByType 本身及其使用都是类型安全的,我会更喜欢它,但如果那不可能,我至少希望 getByType 的用法不需要​​任何不安全的类型断言。

我可以更改容器类型的定义,但实际数据是固定的。 (对于背景:xml2js XML 解析器。)

我们可以使用inferExtract来达到目的。考虑:

const getByType = <ContainerType extends Container<string>, Type extends ContainerType extends Container<infer T> ? T : never, Chosen extends Type>(data: ContainerType[], type: Chosen) => {
    for (const c of data) {
        if (c.type === type) return c as Extract<ContainerType, {type: Chosen}>;
    }
    throw new Error(`No element of type ${type} found.`);
};

const containerA: AContainer = {
  dataA: 1,
  type: "a"
} 
const containerB: BContainer = {
  dataB: true,
  type: "b"
}
const b = getByType([containerB, containerA], 'b')
// b is infered as BContainer 

注意事项:

  • type: ContainerType extends Container<infer T> ? T : never 我们说参数需要包含给定数组中的确切可用类型
  • Extract<ContainerType, {type: Chosen}> 我们说我们 return 与 {type: Chosen} 并集的元素,这意味着具有此确切类型的成员

我们在这里对第二个参数也有严格的类型,在示例中缩小为 a | b

Playground