Программирование

Настройка модификаторов типов 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

Модификаторы типов — это утилитарные типы 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:

typescript
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 — рекурсивный NonNullable, убирает null из всех уровней. Базовый NonNullable из TS docs, но deep-версия кастомная.

Проблема: комбо этих утилит создает opaque типы. В Stack Overflow подтверждают — нет встроенного “unfold all”. Но хак: промежуточные типы.

Создайте Pretty = { [K in keyof T]: T[K] } & { [K in Exclude<keyof T, keyof any>]: T[K] }; — это flattening для IDE.


Как избавиться от 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>.

Шаги фикса:

  1. Замените any на union: type SafeRecord = Record<string, string | number | boolean>;

  2. В tsconfig.json: “strict”: true, “noImplicitAny”: true — это заблокирует any.

  3. Для where-input: type UserWhere = Partial & AtLeast<Partial, ‘id’ | ‘email’>;

Но Partial может дать Record если User — index signature. Проверьте базовый User — нет ли [key: string]: any?

В Dmitri Pavlutin: Record только для известных ключей. Для open objects — лучше interface с index: { [key: string]: UserValue }.

Тестировал: после Replace any → union, тултип стал показывать свойства вместо Record.


Практический пример для UserWhereOneInput

Возьмем ваш UserWhereOneInput. Предположим базовый:

typescript
interface UserBase {
 id: string;
 email: string;
 name?: string;
 data: Record<string, any>; // Вот источник беды!
}

UserCreateInput = Expand<DeepNonNull> — раскрывается, потому что Create — full fields.

UserWhereOneInput = AtLeast<Partial<DeepNonNull>, ‘id’ | ‘email’> — не раскрывается из-за Partial + Record.

Фикс:

typescript
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:

  1. Extension: TypeScript Importer, TypeScript Toolbox — ускоряют infer.

  2. settings.json:

json
{
 "typescript.suggest.autoImports": true,
 "typescript.preferences.includePackageJsonAutoImports": "on",
 "typescript.inlayHints.parameterTypes.enabled": true,
 "typescript.inlayHints.propertyDeclarationTypes.enabled": true
}
  1. tsconfig.json:
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% тултипов.



Источники

  1. Simplifying Complex Type Display in TypeScript and VS Code
  2. Revealing Compound Types in Typescript
  3. TypeScript: Documentation - Utility Types
  4. GitHub - piotrwitek/utility-types
  5. Is it possible to display the full computed type of a typescript type/interface in VSCode
  6. TypeScript Record Type with Examples
  7. Level up your TypeScript with Record types
  8. Record Type in TypeScript: A Quick Intro
  9. The ultimate guide to TypeScript utility types
  10. 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, а разработка ускорится. Если остались вопросы — уточните код!

Авторы
Проверено модерацией
Модерация
Настройка модификаторов типов TypeScript для IDE подсказок