Полное руководство: Тип массива TypeScript со всеми значениями объекта
Узнайте, как создавать типы массивов TypeScript для проверки наличия всех значений объекта в любом порядке. Полное руководство с примерами столбцов таблиц и рабочими реализациями.
Как создать тип массива TypeScript, который содержит все значения из свойств объекта, независимо от порядка?
Дано определение типа объекта, например:
type ExampleObject = {
property1: "abc",
property2: "xyz",
property3: "123"
}
Как определить тип, который удовлетворяется только массивами, содержащими эти точные значения в любом порядке, например:
const exampleArray = ["abc", "xyz", "123"]
const exampleArray2 = ["xyz", "abc", "123"]
В моем конкретном случае использования я работаю с определениями столбцов таблиц:
export type TableColumn<
RowEntity extends object,
ColumnKey extends string,
ValueType,
> = {
getValue: (row: RowEntity) => ValueType;
key: ColumnKey;
};
export type TableStructure = {
name: string;
description: string;
active: boolean;
};
Я уже создал тип для объекта, содержащего все ключи TableStructure с соответствующими TableColumns:
type TableColumnsObject<
TableStructure extends object,
RowEntity extends object,
> = {
[ColumnKey in keyof TableStructure]: TableColumn<
RowEntity,
Extract<ColumnKey, string>,
TableStructure[ColumnKey]
>;
};
Однако мне нужно определить тип для массива TableColumns, который включает все столбцы из TableStructure, независимо от порядка, например:
const columns = [
TableColumn<RowEntity, "name", string>,
TableColumn<RowEntity, "description", string>,
TableColumn<RowEntity, "active", boolean>
]
Как создать тип TypeScript, который проверяет, что массив содержит все необходимые TableColumns на основе TableStructure, при этом столбцы могут быть в любом порядке?
Создание типа массива TypeScript, содержащего все значения из свойств объекта независимо от порядка
Чтобы создать тип массива TypeScript, который содержит все значения из свойств объекта независимо от порядка, необходимо использовать комбинацию типов объединения (union types), отображаемых типов (mapped types) и условных типов (conditional types) для обеспечения полноты (exhaustiveness). Ключевым моментом является сначала извлечение всех значений из объекта в тип объединения, а затем создание типа валидации, который гарантирует, что массив содержит все эти значения в любом порядке.
Содержание
- Извлечение значений из свойств объекта
- Создание полной валидации массива
- Решение для случая использования столбцов таблицы
- Полная рабочая реализация
- Ограничения и альтернативные подходы
Извлечение значений из свойств объекта
Чтобы извлечь все значения из свойств объекта в тип объединения, можно использовать индексные типы доступа с typeof obj[keyof obj]. Этот подход работает как для примитивных значений, так и для сложных типов.
type ExampleObject = {
property1: "abc";
property2: "xyz";
property3: "123";
};
// Извлекаем все значения в тип объединения
type ExampleObjectValues = typeof ExampleObject[keyof ExampleObject];
// Результат: "abc" | "xyz" | "123"
Этот метод работает, потому что keyof ExampleObject produces "property1" | "property2" | "property3", а typeof ExampleObject[...] обращается к типу значения для каждого ключа, создавая объединение.
Для массивов можно использовать сигнатуру индекса [number], чтобы получить объединение всех элементов массива:
const exampleArray = ["abc", "xyz", "123"] as const;
type ExampleArrayValues = typeof exampleArray[number];
// Результат: "abc" | "xyz" | "123"
Утверждение as const здесь критически важно - оно предотвращает расширение типа до string[] и сохраняет литеральные значения в объединении.
Создание полной валидации массива
Сложность заключается в создании типа, который проверяет, что массив содержит все значения из объединения, а не только некоторые из них. Для этого требуется более продвинутая манипуляция типами.
На основе результатов исследования, вот комплексный подход:
// Сначала создаем тип, который проверяет, равны ли два типа
type Equals<A, B> = A extends B ? B extends A ? true : false : false;
// Затем создаем тип, который проверяет, что массив содержит все элементы объединения
type ExhaustiveArray<T, U = T> = T extends [infer First, ...infer Rest]
? [Exclude<U, First>] extends [never]
? [First]
: [First, ...ExhaustiveArray<Rest, Exclude<U, First>>]
: [];
Этот рекурсивный тип работает следующим образом:
- Принимает тип объединения
Tи обрабатывает его элементы по одному - Использует
Exclude<U, First>, чтобы удалить каждое совпавшее значение из оставшегося объединения - Когда все значения совпадают (
Exclude<U, First>становитсяnever), рекурсия прекращается
Однако этот подход имеет ограничения при работе со сложными типами, такими как ваши типы TableColumn. Более практическое решение основано на исследованиях полных массивов:
// Создаем тип, который проверяет, что массив содержит все элементы объединения
type ContainsAll<T, U> = U extends T ? true : false;
// Создаем тип полного массива
type ExhaustiveArray<T, U = T> = T extends any
? [T] extends [U]
? []
: [T, ...ExhaustiveArray<Exclude<U, T>>]
: [];
Решение для случая использования столбцов таблицы
Для вашего конкретного случая использования столбцов таблицы, вот полное решение:
export type TableColumn<
RowEntity extends object,
ColumnKey extends string,
ValueType,
> = {
getValue: (row: RowEntity) => ValueType;
key: ColumnKey;
};
export type TableStructure = {
name: string;
description: string;
active: boolean;
};
// Сначала создаем тип TableColumnsObject
type TableColumnsObject<
TableStructure extends object,
RowEntity extends object,
> = {
[ColumnKey in keyof TableStructure]: TableColumn<
RowEntity,
Extract<ColumnKey, string>,
TableStructure[ColumnKey]
>;
};
// Извлекаем все типы TableColumn в объединение
type TableColumnUnion<
TableStructure extends object,
RowEntity extends object,
> = TableColumnsObject<TableStructure, RowEntity>[keyof TableColumnsObject<TableStructure, RowEntity>];
// Создаем тип, который проверяет, что массив содержит все необходимые TableColumn
type TableColumnsArray<
TableStructure extends object,
RowEntity extends object,
U = TableColumnUnion<TableStructure, RowEntity>,
> = T extends [infer First, ...infer Rest]
? [Exclude<U, First>] extends [never]
? [First]
: [First, ...TableColumnsArray<Rest, Exclude<U, First>>]
: [];
// Пример использования:
type RowEntity = { name: string; description: string; active: boolean };
// Этот тип будет принимать только массивы, содержащие все три типа TableColumn
type ValidTableColumns = TableColumnsArray<TableStructure, RowEntity>;
// Валидно
const validColumns: ValidTableColumns = [
{ getValue: (row) => row.name, key: "name" },
{ getValue: (row) => row.description, key: "description" },
{ getValue: (row) => row.active, key: "active" }
];
// Также валидно (другой порядок)
const validColumns2: ValidTableColumns = [
{ getValue: (row) => row.active, key: "active" },
{ getValue: (row) => row.name, key: "name" },
{ getValue: (row) => row.description, key: "description" }
];
// Невалидно - отсутствует столбец "active"
const invalidColumns: ValidTableColumns = [
{ getValue: (row) => row.name, key: "name" },
{ getValue: (row) => row.description, key: "description" }
];
Полная рабочая реализация
Вот более практическая реализация, которая лучше работает со сложными типами и предоставляет более информативные сообщения об ошибках:
// Утилитарные типы для полной валидации массива
type NotAny<T> = T extends any ? false : true;
type IsNever<T> = NotAny<T> extends true ? false : keyof T extends never ? true : false;
// Создаем тип, который проверяет, что массив содержит все элементы объединения
type ExhaustiveArray<T, U = T> = T extends any
? IsNever<Exclude<U, T>> extends true
? [T]
: [T, ...ExhaustiveArray<Exclude<U, T>>]
: [];
// Для случая использования столбцов таблицы
type TableColumnsArray<
TableStructure extends object,
RowEntity extends object,
U = TableColumnUnion<TableStructure, RowEntity>,
> = ExhaustiveArray<U[number]>; // Используем объединение всех типов TableColumn
// Альтернативный подход с использованием значений объекта
type ObjectValues<T> = T[keyof T];
// Создаем тип, который проверяет, что массив содержит все значения объекта
type ContainsAllObjectValues<T, Values = ObjectValues<T>> = Values extends any
? [Values] extends [T extends readonly any[] ? T[number] : never]
? true
: false
: false;
// Использование с вашей структурой таблицы
type RowEntity = { name: string; description: string; active: boolean };
// Это будет проверять, что массив содержит все необходимые столбцы
type ValidTableColumnsArray<T extends readonly TableColumn<RowEntity, any, any>[]> =
ContainsAllObjectValues<T> extends true ? T : never;
// Валидное использование
const validTableColumns: ValidTableColumnsArray<[
TableColumn<RowEntity, "name", string>,
TableColumn<RowEntity, "description", string>,
TableColumn<RowEntity, "active", boolean>
]> = [
{ getValue: (row) => row.name, key: "name" },
{ getValue: (row) => row.description, key: "description" },
{ getValue: (row) => row.active, key: "active" }
];
// Невалидное использование (вызовет ошибку типа)
const invalidTableColumns: ValidTableColumnsArray<[
TableColumn<RowEntity, "name", string>,
TableColumn<RowEntity, "description", string>
]> = [
{ getValue: (row) => row.name, key: "name" },
{ getValue: (row) => row.description, key: "description" }
];
Ограничения и альтернативные подходы
Ограничения:
-
Сопоставление сложных типов: Система типов TypeScript имеет ограничения при сравнении сложных объектных типов. Типы
TableColumnс разными обобщенными параметрамиValueTypeмогут не совпадать правильно из-за различий в параметрах обобщенных типов. -
Независимость от порядка: Как отмечено в исследованиях, кортежи (tuples) по своей природе упорядочены, в то время как объединения (unions) нет, что делает идеальную проверку полноты сложной задачей.
-
Производительность: Сложные рекурсивные типы могут влиять на производительность проверки типов TypeScript при работе с большими объединениями.
Альтернативные подходы:
- Валидация во время выполнения: Для критически важной валидации объедините компиляционную проверку типов с проверками во время выполнения:
function validateTableColumns<T extends TableStructure, R extends object>(
columns: TableColumn<R, any, any>[],
structure: T
): columns is ValidTableColumnsArray<T, R> {
const requiredKeys = Object.keys(structure);
const columnKeys = columns.map(col => col.key);
return requiredKeys.every(key => columnKeys.includes(key)) &&
columnKeys.length === requiredKeys.length;
}
// Использование
const testColumns = [/* ваш массив столбцов */];
if (validateTableColumns(testColumns, {} as TableStructure)) {
// TypeScript теперь знает, что это валидно
}
- Упрощенный подход: Если точное сопоставление типов не является критически важным, используйте более простой подход на основе объединения:
type SimpleTableColumnsArray<T extends TableStructure> =
Array<{
key: keyof T;
getValue: (row: any) => T[keyof T];
}>;
- Брендированные типы (Brand Types): Для лучшей безопасности типов рассмотрите использование брендированных типов:
type BrandedTableColumn<T> = TableColumn<RowEntity, string, any> & { __brand: T };
type ValidTableColumns = [
BrandedTableColumn<"name">,
BrandedTableColumn<"description">,
BrandedTableColumn<"active">
];
Как объясняется в документации Microsoft по TypeScript, система типов TypeScript предоставляет мощные инструменты для создания сложных типов, но существуют inherent ограничения при работе с динамическими коллекциями и проверкой полноты.
Наиболее надежным решением для вашего случая использования будет комбинация компиляционных ограничений типов и валидации во время выполнения для обеспечения как безопасности типов, так и фактической корректности.
Источники
- Создание типа объединения из массива или объекта в TypeScript | bobbyhadz
- typescript - Вывод типа объединения из значений кортежа/массива - Stack Overflow
- Полный массив на типе объединения · Issue #53171 · microsoft/TypeScript
- TypeScript: Документация - Повседневные типы
- Как вывести тип объединения из значений кортежа/массива в TypeScript?