TypeScript: выбрать элемент массива по свойству типа - PullRequest
1 голос
/ 11 февраля 2020

В проекте 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, с полной безопасностью типа. Примерно так:

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 в том, что функция безопасна для типов, а возвращаемое значение является элементом исходного массива и имеет запрошенный тип.

Вот моя лучшая попытка:

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>, и что тип возврата в примере вызова AContainer | (Container<"b"> & { dataB: boolean; } & Container<"a">), равно AContainer из-за конфликта в Container<"b"> & Container<"a">. Первая проблема может быть решена путем использования предиката типа в качестве следующего в следующем блоке кода (хотя это похоже на обман), но я не нашел решения для второй проблемы.

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

Есть ли способ заставить это работать? Я бы предпочел, чтобы как getByType, так и само его использование были безопасными с точки зрения типов, но если это невозможно, я бы хотел, чтобы по крайней мере использование getByType не требовало небезопасных утверждений типа.

I Можно изменить определения типов контейнеров, но фактические данные фиксированы. (Для фона: xml2 js XML парсер.)

1 Ответ

1 голос
/ 11 февраля 2020

Мы можем использовать infer и Extract для достижения цели. Рассмотрим:

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}> мы говорим, что возвращаем элемент объединения с {type: Chosen}, и это означает член с этим точным типом

Здесь мы также имеем строгий тип по второму аргументу, в примере суженный до a | b

Детская площадка

...