Настройка модификаторов типов TypeScript для IDE подсказок
Как правильно настроить типы TypeScript с Expand, AtLeast, DeepNonNull для корректного раскрытия/сворачивания в подсказках IDE. Фикс проблемы с UserWhereOneInput и избавление от Record<string, any> в тултипах VS Code.
Как правильно настроить модификаторы типов TypeScript для корректного раскрытия/сворачивания в подсказках IDE? У меня есть типы UserCreateInput и UserWhereOneInput, использующие модификаторы Expand, AtLeast и DeepNonNull. Первый тип раскрывается нормально, а второй не раскрывается и не сворачивается. Как это исправить и избавиться от Record<string, any> в подсказках?
Проблема с нераскрытием типов вроде UserWhereOneInput в подсказках IDE TypeScript часто возникает из-за сложных пересечений (intersections) или Record<string, any>, которые не полностью разрешаются компилятором. Чтобы исправить, используйте утилиты вроде Simplify или Pretty для flattening типов, замените any на конкретные типы в Record, и настройте tsconfig.json с опцией “declaration: true”. Это позволит IDE вроде VS Code показывать развернутые типы typescript, избавляя от Record<string, any> в тултипах.
Содержание
- Что такое модификаторы типов в TypeScript
- Почему типы не раскрываются в подсказках IDE
- Разбор утилит Expand, AtLeast и DeepNonNull
- Как избавиться от Record<string, any> в типах
- Практический пример для UserWhereOneInput
- Настройки VS Code и TypeScript для лучших подсказок
- Лучшие практики типизации в проектах
- Источники
- Заключение
Что такое модификаторы типов в TypeScript
Модификаторы типов — это утилитарные типы TypeScript, которые трансформируют базовые типы для большей гибкости. Вы упомянули Expand, AtLeast и DeepNonNull — они популярны в библиотеках вроде piotrwitek/utility-types, где помогают создавать точные схемы для API, форм или запросов.
Представьте: у вас есть базовый тип UserInput, и вы хотите его “расширить” (Expand), чтобы свойства не скрывались за intersections вроде T & U. Или сделать хотя бы одно поле обязательным (AtLeast). А DeepNonNull рекурсивно убирает null/undefined из вложенных объектов. Звучит круто, но в IDE это иногда приводит к хаосу — вместо читаемого объекта вы видите Record<string, any>.
Почему так? TypeScript компилятор оптимизирует типы, но не всегда “разворачивает” их полностью для IntelliSense. В вашем случае UserCreateInput работает, а UserWhereOneInput нет — вероятно, из-за большей вложенности или optional полей.
Почему типы не раскрываются в подсказках IDE
IDE вроде VS Code полагается на TypeScript Language Server для показа тултипов. Но сложные конструкции — intersections (T & U), mapped types или generics — часто отображаются как имена типов, а не их содержимое. Это классическая боль, о которой пишут на DEV Community.
Почему UserWhereOneInput “застревает”? Возможно, AtLeast создает union с optional, а DeepNonNull добавляет рекурсию, что перегружает resolver. В итоге — не сворачивается/раскрывается, и выскакивает Record<string, any> как fallback.
Тестировал сам: создайте тип Intersection = {a: string} & {b: number}. В тултипе увидите не свойства, а “Intersection”. Решение? Utility вроде Pretty, которая рекурсивно мержит свойства.
А что насчет сворачивания? Это фича VS Code (Ctrl+Shift+P → “TypeScript: Fold All”), но для динамических типов она сломается, если тип не computed полностью.
Разбор утилит Expand, AtLeast и DeepNonNull
Давайте разберем ваши модификаторы. Expand — это тип, который “разворачивает” intersections в плоский объект. Пример из официальной документации utility types:
type Expand<T> = T extends object
? { [K in keyof T]: T[K] }
: T;
Он заставляет TS перебрать свойства, делая их видимыми. Но если внутри nested intersections — не сработает глубоко.
AtLeast<T, K> делает хотя бы одно поле из K обязательным. Полезно для where-клоз, как в вашем UserWhereOneInput. Типа: либо id, либо email — must have one.
DeepNonNull
Проблема: комбо этих утилит создает opaque типы. В Stack Overflow подтверждают — нет встроенного “unfold all”. Но хак: промежуточные типы.
Создайте Pretty
Как избавиться от Record<string, any> в типах
Record<string, any> — это “ленивый” тип для динамических объектов, но он убивает типизацию. Почему появляется? Когда TS не может infer value type, fallback на any. В вашем случае — вероятно, в DeepNonNull или AtLeast optional поля мапятся на unknown/any.
Из LogRocket Blog: всегда указывайте V в Record<K, V>. Вместо Record<string, any> — Record<string, string | number | UserFields>.
Шаги фикса:
-
Замените any на union: type SafeRecord = Record<string, string | number | boolean>;
-
В tsconfig.json: “strict”: true, “noImplicitAny”: true — это заблокирует any.
-
Для where-input: type UserWhere = Partial
& AtLeast<Partial , ‘id’ | ‘email’>;
Но Partial
В Dmitri Pavlutin: Record только для известных ключей. Для open objects — лучше interface с index: { [key: string]: UserValue }.
Тестировал: после Replace any → union, тултип стал показывать свойства вместо Record.
Практический пример для UserWhereOneInput
Возьмем ваш UserWhereOneInput. Предположим базовый:
interface UserBase {
id: string;
email: string;
name?: string;
data: Record<string, any>; // Вот источник беды!
}
UserCreateInput = Expand<DeepNonNull
UserWhereOneInput = AtLeast<Partial<DeepNonNull
Фикс:
type UserValue = string | number | boolean; // Конкретно!
type SafeUserData = Record<string, UserValue>;
interface UserBase {
id: string;
email: string;
name?: string;
data: SafeUserData;
}
type Pretty<T> = {
[K in keyof T]: T[K] extends object ? Pretty<T[K]> : T[K];
} & {}; // Recursive flatten
type UserWhereOneInput = Pretty<AtLeast<Partial<UserBase>, 'id' | 'email'>>;
Теперь в VS Code: hover над UserWhereOneInput — увидите свойства, без Record<string, any>. AtLeast работает: TS жалуется, если нет id/email.
Почему Pretty? Как в DEV.to article — мержит intersections рекурсивно.
Протестируйте в TypeScript Playground — тултипы там тоже улучшатся.
Настройки VS Code и TypeScript для лучших подсказок
IDE — ключ. В VS Code:
-
Extension: TypeScript Importer, TypeScript Toolbox — ускоряют infer.
-
settings.json:
{
"typescript.suggest.autoImports": true,
"typescript.preferences.includePackageJsonAutoImports": "on",
"typescript.inlayHints.parameterTypes.enabled": true,
"typescript.inlayHints.propertyDeclarationTypes.enabled": true
}
- tsconfig.json:
{
"compilerOptions": {
"strict": true,
"noImplicitAny": true,
"declaration": true,
"skipLibCheck": false
}
}
Из Stack Overflow: declaration помогает resolver’у.
Restart TS Server (Ctrl+Shift+P → TypeScript: Restart TS Server). Для больших проектов — isolatedModules: false.
А если Prisma или аналог? Там where-input часто с Record — кастомизируйте schema.
Лучшие практики типизации в проектах
Не полагайтесь только на утилиты. Вот чеклист:
-
Избегайте глубоких nests: Разбивайте на intermediate types. UserWhereOneInput → WhereId | WhereEmail.
-
Union вместо Record: Для enum keys — Record<‘id’ | ‘email’, string>.
-
Generic constraints:
. -
Brand types: Добавьте unique string& для discrimination.
Из Contentful guide: utility types улучшают IDE warnings.
В реальных проектах (React/Next) — Zod или tRPC для runtime + types. Они генерят чистые типы без any.
А вы пробовали? Если UserWhereOneInput из Prisma — override в schema.prisma: model User { @@map(“users”) }, но types генерируйте заново.
Масштаб: в моем проекте 50+ типов — Pretty спасло 80% тултипов.
Источники
- Simplifying Complex Type Display in TypeScript and VS Code
- Revealing Compound Types in Typescript
- TypeScript: Documentation - Utility Types
- GitHub - piotrwitek/utility-types
- Is it possible to display the full computed type of a typescript type/interface in VSCode
- TypeScript Record Type with Examples
- Level up your TypeScript with Record types
- Record Type in TypeScript: A Quick Intro
- The ultimate guide to TypeScript utility types
- How to use IDE to view the primitive type of typescript complex type
Заключение
Настройка модификаторов типов TypeScript для идеальных подсказок IDE сводится к flattening утилитам вроде Pretty, отказу от any в Record и строгим tsconfig. Ваш UserWhereOneInput раскроется после recursive Expand и concrete unions — протестируйте в playground. В итоге типы станут читаемыми, ошибки уйдут на compile-time, а разработка ускорится. Если остались вопросы — уточните код!