Как понять, что команда использует TypeScript идиоматично, а не переносит привычки из другого языка?
Признаки идиоматичного TS: включённый strict, utility types вместо дублирования интерфейсов, discriminated union с exhaustive check, type guard вместо as, минимум any — всё это легко проверяется через tsconfig и ESLint.
Признаки идиоматичного TypeScript в кодовой базе
Переход из Java, C# или Python оставляет следы в коде: паттерны правильные по логике, но написаны не в духе TypeScript. Разберём конкретные сигналы и как их диагностировать.
Code review: на что смотреть
Первый уровень — статический анализ pull request'ов. Смотрим на:
- Частота as и any — запустите
grep -rn ' as ' src/ | wc -lиgrep -rn ': any' src/ | wc -l. В идиоматичном коде их единицы. - Type guard vs type assertion — есть ли функции с
value is Tв сигнатуре, или всё делается черезas T? - Utility types — используются ли
Partial<T>,Required<T>,Pick<T, K>,Omit<T, K>,ReturnType<F>,Parameters<F>? Если везде дублируются интерфейсы вручную — это признак Java-мышления.
// не идиоматично: ручное копирование полей
interface UserPreview {
id: string;
name: string;
}
// идиоматично: derive from source of truth
type UserPreview = Pick<User, 'id' | 'name'>;
Проверка конфигурации
Откройте tsconfig.json. Идиоматичная команда включает строгий режим полностью:
{
"compilerOptions": {
"strict": true,
"noUncheckedIndexedAccess": true,
"exactOptionalPropertyTypes": true,
"noImplicitOverride": true
}
}
Если strict выключен или отключены отдельные флаги (strictNullChecks: false) — команда избегает TS-ошибок вместо того, чтобы их решать.
Паттерны, выдающие другой язык
// Java-паттерн: getter/setter методы в классе
class UserService {
private _user: User | null = null;
getUser(): User | null { return this._user; }
setUser(u: User): void { this._user = u; }
}
// TypeScript-идиоматично: функции + иммутабельные структуры
type UserState = { readonly user: User | null };
const withUser = (state: UserState, user: User): UserState => ({ ...state, user });
// Python-паттерн: dict с any
const config: { [key: string]: any } = {};
// TypeScript-идиоматично: discriminated union
type Config =
| { mode: 'production'; apiUrl: string }
| { mode: 'development'; mockDelay: number };
Работа с discriminated unions
Идиоматичный TypeScript широко использует discriminated union + exhaustive check через never:
type Shape =
| { kind: 'circle'; radius: number }
| { kind: 'square'; side: number };
function area(shape: Shape): number {
switch (shape.kind) {
case 'circle': return Math.PI * shape.radius ** 2;
case 'square': return shape.side ** 2;
default: {
const _exhaustive: never = shape;
throw new Error(`Unknown shape: ${JSON.stringify(_exhaustive)}`);
}
}
}
Если команда не знает этого паттерна и использует if/else с as — это явный перенос привычек.
ESLint как автоматическая диагностика
Включите @typescript-eslint/recommended-requiring-type-checking и смотрите на количество предупреждений. Правила no-explicit-any, no-unsafe-assignment, no-floating-promises выявляют неидиоматичный код автоматически.
Подводные камни
- Команда отключает eslint-правила через disable-комментарии —
// eslint-disable-next-line @typescript-eslint/no-explicit-anyв каждом файле хуже, чем единый eslint-disable в конфиге. - Overuse классов — в TypeScript классы нужны реже, чем в Java; функции + типы часто чище и лучше tree-shake'ятся.
- Enum вместо const object / union — числовые enum сериализуются непредсказуемо, строковые union прозрачнее.
- Отсутствие readonly — иммутабельность не выражена в типах, мутации происходят случайно.
- Ручные типы вместо infer/ReturnType — дублирование типов ломается при рефакторинге.
- Promise без await обёртки и без catch — floating promise не поймать без
no-floating-promiseseslint-правила. - Неиспользование satisfies оператора (TS 4.9+) — команды, пришедшие со старым бэкграундом, не знают об этом операторе для валидации без widening типа.
What hurts your answer
- Сразу обвинять TypeScript, не проверив соседние слои системы
- Чинить симптом без минимального воспроизведения и evidence
- Не учитывать версии, конфигурацию, окружение и recent changes
What they're listening for
- Умеет локализовать проблему вокруг TypeScript
- Двигается от симптома к гипотезам и проверкам
- Отличает баг инструмента от ошибки использования или окружения