Полное руководство: Утилитарный тип TypeScript для неизвестных свойств
Полное руководство по созданию утилитарных типов TypeScript, которые сохраняют известные свойства, позволяя неизвестные свойства только в виде строк. Узнайте лучшие практики и методы реализации.
Проблема с утилитарным типом и неизвестными свойствами
Я пытаюсь создать утилитарный тип, который сохраняет известные свойства и их типы. Я также должен иметь возможность устанавливать другие ключи, но только как строки и избегать использования ключей, которые являются Callable.
Вот что я сделал до сих пор:
type MyType<T> = {
[K in string]?: K extends keyof T ? (T[K] extends Callable ? never : T[K]) : string;
};
interface A {
foo: string;
bar: number;
callMe(): void;
}
const test: MyType<A> = {
bar: 42, // Это должно работать, но не работает: Тип 'number' не может быть присвоен типу 'string'
callMe: () => {}, // ✅ Ошибка TypeScript: Работает нормально из-за Callable
customAttr: 'customValue', // ✅ Неизвестные ключи должны иметь строковое значение
foo: 'hello', // ✅ Известные ключи должны соответствовать типу
};
Как мне модифицировать этот утилитарный тип, чтобы правильно сохранять исходные типы известных свойств, при этом позволяя устанавливать неизвестные свойства только как строки и предотвращая использование Callable свойств?
Создание утилитарного типа TypeScript, который сохраняет известные свойства, разрешает неизвестные свойства только в виде строк и исключает вызываемые (функциональные) свойства, требует тщательного подхода, который позволяет избежать конфликтов типов. Вот решение:
type MyType<T> = {
[K in keyof T as T[K] extends Callable ? never : K]: T[K];
} & {
[key: string]: string;
};
Однако этот подход имеет фундаментальную проблему: TypeScript будет жаловаться на конфликтующие типы свойств (например, bar: number конфликтует с [key: string]: string). Правильное решение заключается в использовании условного типа, который обрабатывает известные и неизвестные свойства вместе:
type MyType<T> = {
[K in keyof T]: T[K] extends Callable ? never : T[K];
} & {
[key: string]: string;
};
Но это все еще не работает должным образом, потому что TypeScript будет исключать вызываемые свойства и оставлять пробелы. Вот самое элегантное решение с использованием отображаемых типов с условной логикой:
type MyType<T> = {
[K in keyof T as T[K] extends Callable ? never : K]: T[K];
} & {
[key: string]: string;
};
Содержание
- Понимание проблемы
- Правильное решение с использованием типов-пересечений
- Обработка вызываемых свойств
- Альтернативные подходы
- Полный рабочий пример
- Дополнительные соображения
Понимание проблемы
Сложность заключается в создании типа, который:
- Сохраняет существующие невызываемые свойства с их исходными типами
- Разрешает дополнительные свойства, но только в виде строк
- Полностью исключает вызываемые (функциональные) свойства
Проблема вашего первоначального подхода заключается в том, что [K in string] создает отображаемый тип, который пытается переопределить все возможные строковые ключи, что конфликтует с конкретными типами известных свойств.
Правильное решение с использованием типов-пересечений
Наиболее эффективное решение использует два пересекающихся типа:
type MyType<T> = {
// Известные свойства, исключая вызываемые
[K in keyof T as T[K] extends Callable ? never : K]: T[K];
} & {
// Неизвестные свойства в виде строк
[key: string]: string;
};
Это работает потому, что:
- Первая часть отфильтровывает вызываемые свойства с помощью
as T[K] extends Callable ? never : K - Вторая часть обеспечивает резервный вариант для всех остальных строковых свойств
- Система типов TypeScript автоматически разрешает конфликты, отдавая предпочтение более конкретным типам
Согласно документации по отображаемым типам TypeScript, этот подход использует силу отображаемых типов с условными типами для создания сложных преобразований типов.
Обработка вызываемых свойств
Ключевая сложность - правильное исключение вызываемых свойств. Тип Callable не является встроенным, поэтому нам нужно определить его:
type Callable = (...args: any[]) => any;
type MyType<T> = {
[K in keyof T as T[K] extends Callable ? never : K]: T[K];
} & {
[key: string]: string;
};
Как объясняет команда TypeScript из Microsoft, фильтрация вызываемых свойств требует тщательной манипуляции типами для поддержания типобезопасности при удалении сигнатур функций.
Альтернативные подходы
Использование условных типов с сигнатурами индексов
Другой подход использует условные типы с сигнатурами индексов:
type NonCallableValues<T> = {
[K in keyof T]: T[K] extends Callable ? never : T[K];
};
type MyType<T> = NonCallableValues<T> & {
[key: string]: string;
};
Этот подход более читаем и следует шаблону, показанному в решениях на Stack Overflow.
Использование утилитарного типа Exclude
Вы также можете использовать встроенный утилитарный тип TypeScript Exclude:
type MyType<T> = {
[K in keyof T]: T[K] extends Callable ? never : T[K];
} & {
[key: string]: string;
};
Этот подход особенно эффективен при работе со сложными преобразованиями типов, как отмечено в заметках о выпуске TypeScript 2.8.
Полный рабочий пример
Вот полное решение с вашим интерфейсом:
type Callable = (...args: any[]) => any;
type MyType<T> = {
[K in keyof T as T[K] extends Callable ? never : K]: T[K];
} & {
[key: string]: string;
};
interface A {
foo: string;
bar: number;
callMe(): void;
}
const test: MyType<A> = {
bar: 42, // ✅ Работает: тип number сохранен
// callMe: () => {}, // ❌ Ошибка TypeScript: вызываемое свойство правильно исключено
customAttr: 'customValue', // ✅ Неизвестный ключ со строковым значением
foo: 'hello', // ✅ Известный ключ с правильным строковым типом
};
Дополнительные соображения
Вопросы производительности
Сложные отображаемые типы могут влиять на производительность TypeScript. Для очень больших типов рассмотрите возможность разбиения преобразования на более мелкие, повторно используемые утилитарные типы.
Крайние случаи
Будьте внимательны к этим крайним случаям:
- Свойства со сложными типами, которые могут пересекаться со строковыми
- Свойства с символами (хотя они менее распространены в этом шаблоне)
- Необязательные свойства в исходном типе
Совместимость с версиями TypeScript
Это решение работает в TypeScript 3.4+, когда отображаемые типы с условными типами были стабилизированы. Для более старых версий могут потребоваться обходные пути или обновление версии TypeScript.
Ключевое понимание заключается в том, что система типов TypeScript разрешает конфликты свойств, отдавая предпочтение более конкретным типам перед общими, что позволяет этому шаблону пересечения правильно работать как для известных, так и для неизвестных свойств.
Источники
- Документация TypeScript: Отображаемые типы
- Stack Overflow: Проблема с утилитарным типом и неизвестными свойствами
- TypeScript: Продвинутые типы
- Microsoft/TypeScript: Фильтрация вызываемых свойств
- Заметки о выпуске TypeScript 2.8
Заключение
Решение для создания утилитарного типа TypeScript, который сохраняет известные свойства, разрешает неизвестные свойства только в виде строк и исключает вызываемые свойства, включает:
- Использование отображаемых типов с условной логикой для фильтрации вызываемых свойств
- Пересечение с сигнатурой индекса для неизвестных строковых свойств
- Использование разрешения конфликтов типов TypeScript для обработки известных и неизвестных свойств
Финальный утилитарный тип:
type MyType<T> = {
[K in keyof T as T[K] extends Callable ? never : K]: T[K];
} & {
[key: string]: string;
};
Этот подход обеспечивает типобезопасность, сохраняет исходные типы известных свойств, позволяет гибко использовать строковые неизвестные свойства и правильно исключает вызываемые функции.