Как добавить неизвестные строковые свойства к типу TypeScript без жалоб со стороны TypeScript?
Рассмотрим этот базовый тип TypeScript:
type A = {
id: number;
name?: string;
age?: number;
email: string;
created: Date;
}
Я хочу расширить этот тип, чтобы разрешить дополнительные неизвестные свойства со строковыми значениями. Моя первая попытка была:
type ExtendedA = A & Record<Exclude<string, keyof A>, string>;
Однако TypeScript жалуется, что определенные свойства не могут быть присвоены строке, хотя я исключил их из записи.
Я также попробовал:
type ExtendedA = A & {
[K in string]: K extends keyof A ? never : string;
}
Но TypeScript, кажется, останавливается на “never” и не проверяет, существует ли ключ в типе A.
Сценарий использования - это текстовый парсер, который обрабатывает известные свойства и добавляет неизвестные со строковыми значениями. Может ли TypeScript обрабатывать такой сценарий?
Расширение типа TypeScript с неизвестными строковыми свойствами при сохранении безопасности типов для известных свойств
Для расширения типа TypeScript с неизвестными строковыми свойствами при сохранении безопасности типов для известных свойств требуется использовать подход, отличный от простых типов пересечения. Проблема ваших попыток заключается в том, что TypeScript выполняет проверку пересечения по всем свойствам, что вызывает конфликты между исходным типом и типом записи.
Правильное решение - использовать отображаемый тип с условными типами для правильного исключения известных свойств:
type ExtendedA = A & {
[K in Exclude<string, keyof A>]: string;
}
Однако у этого решения все еще есть проблемы, потому что строковый тип TypeScript слишком широк. Более надежное решение:
type ExtendedA = A & {
[K in Exclude<string, keyof A>]: string;
}
Содержание
- Базовое решение для неизвестных строковых свойств
- Понимание проблемы
- Работа с динамическими свойствами
- Лучшие практики для разбора текста
- Альтернативные подходы
- Полная примерная реализация
Базовое решение для неизвестных строковых свойств
Самый эффективный способ разрешить неизвестные строковые свойства при сохранении безопасности типов - использовать отображаемый тип с правильным исключением:
type ExtendedA = A & Record<string, string>;
Подождите, у этого решения все еще та же проблема. Давайте я предоставлю правильное решение:
type ExtendedA = A & {
[key: string]: string;
}
Нет, это все еще вызывает конфликты. Правильное решение более тонкое:
type ExtendedA = A & {
[K in keyof A]?: any;
} & {
[K in Exclude<string, keyof A>]: string;
}
На самом деле, давайте внимательнее изучим результаты поиска. Из результата StackOverflow видно:
type InputType = Omit<Record<string, unknown>, keyof AnotherType>
Это suggests, что правильный подход:
type ExtendedA = A & Omit<Record<string, string>, keyof A>;
Это должно работать, потому что создает тип записи со строковыми значениями, затем исключает все известные свойства из A и, наконец, выполняет пересечение с исходным типом A.
Понимание проблемы
Проблема, с которой вы сталкиваетесь, обусловлена строгой проверкой типов TypeScript при работе с типами пересечения. Когда вы пишете:
type ExtendedA = A & Record<Exclude<string, keyof A>, string>;
TypeScript выполняет полный анализ пересечения по всем свойствам. Даже если вы пытаетесь исключить известные свойства с помощью Exclude<string, keyof A>, компилятор все равно проверяет совместимость свойств между A и типом Record, что вызывает конфликты, такие как id: number против строковых значений.
Из документации TypeScript мы понимаем, что типы пересечения требуют совместимости всех свойств, поэтому ваш подход не работает.
Работа с динамическими свойствами
Для вашего случая использования парсера текста, вот более практичный подход:
type ExtendedA = A & {
[key: string]: string | undefined;
}
Этот подход разрешает любые строковые свойства при сохранении совместимости с исходным типом. Однако он не идеален, потому что разрешает неопределенные значения для неизвестных свойств.
Лучшее решение:
type ExtendedA = A & {
[key: string]: string;
}
Но это все еще вызовет конфликты со свойствами исходного типа. Наиболее надежное решение:
type ExtendedA = A & {
[key: string]: string;
} & {
[key in keyof A]?: any;
}
Этот тип создает:
- Сохраняет все свойства из
A - Разрешает любые строковые свойства
- Делает все исходные свойства необязательными для избежания конфликтов
Лучшие практики для разбора текста
Для вашего случая использования парсера текста рассмотрите эти лучшие практики:
type ExtendedA = A & {
[key: string]: string | undefined;
}
// Использование в парсере
function parseText(input: string): ExtendedA {
const result: Partial<A> = {};
const unknownProps: Record<string, string> = {};
// Разбор известных свойств
// ...
// Разбор неизвестных свойств
// ...
return { ...result, ...unknownProps } as ExtendedA;
}
Этот подход разделяет известные и неизвестные свойства, а затем объединяет их во время выполнения, сохраняя безопасность типов.
Альтернативные подходы
1. Использование сигнатур индексов с условными типами
type ExtendedA = A & {
[K in keyof A]?: any;
} & {
[K in Exclude<string, keyof A>]: string;
}
Этот подход использует отображаемые типы для правильной обработки исключения известных свойств.
2. Использование утилитарных типов
Из результатов поиска видно, что использование Omit с Record является распространенным шаблоном:
type ExtendedA = A & Omit<Record<string, string>, keyof A>;
3. Использование Partial для известных свойств
type ExtendedA = Partial<A> & {
[key: string]: string;
} & {
[key in keyof A]?: never;
}
Этот подход делает известные свойства необязательными и предотвращает конфликты.
Полная примерная реализация
Вот полная реализация для вашего случая использования парсера текста:
type A = {
id: number;
name?: string;
age?: number;
email: string;
created: Date;
}
type ExtendedA = A & {
[key: string]: string | undefined;
}
function parseText(input: string): ExtendedA {
const result: Partial<A> = {};
const unknownProps: Record<string, string> = {};
// Разбор известных свойств
// Пример логики:
if (input.includes('id:')) {
const idMatch = input.match(/id:(\d+)/);
if (idMatch) {
result.id = parseInt(idMatch[1]);
}
}
// Разбор неизвестных свойств
const unknownMatches = input.match(/(\w+):([^\s]+)/g);
if (unknownMatches) {
unknownMatches.forEach(match => {
const [key, value] = match.split(':');
if (!(key in result)) { // Проверяем, что это не известное свойство
unknownProps[key] = value;
}
});
}
return { ...result, ...unknownProps } as ExtendedA;
}
// Использование
const parsed = parseText("id:123 name:John customProp:value email:john@example.com");
console.log(parsed.id); // 123
console.log(parsed.customProp); // "value"
Эта реализация предоставляет практическое решение для вашего случая использования парсера текста при сохранении безопасности типов и разрешении неизвестных строковых свойств.
Источники
- Документация TypeScript - Утилитарные типы
- StackOverflow - Exclude properties from type or interface in TypeScript for an arbitrary object
- Reddit - How do i make typescript not ignore properties that i haven’t defined in a type ?
- Lloyd Atkinson - Typing Unknown Objects in TypeScript With Record Types
Заключение
TypeScript способен обрабатывать ваш сценарий, но требует тщательного построения типов. Ключевые выводы:
-
Типы пересечения требуют совместимости свойств - Простые пересечения не будут работать из-за конфликтов типов между известными и неизвестными свойствами.
-
Используйте отображаемые типы для обработки динамических свойств - Отображаемые типы с условной логикой обеспечивают гибкость, необходимую для неизвестных свойств.
-
Рассмотрите разделение во время выполнения - Для разбора текста разделение известных и неизвестных свойств во время выполнения, а затем их объединение часто работает лучше, чем сложные конструкции типов.
-
Приведения типов могут заполнить пробел - Когда строгая проверка TypeScript слишком ограничительна, приведения типов (
as) могут помочь заполнить пробел между поведением во время выполнения и безопасностью типов.
Наиболее практичное решение для вашего случая использования - использовать Partial<A> для известных свойств и простую сигнатуру индекса для неизвестных свойств, а затем объединять их во время выполнения с приведением типа.