TypeScript

100 питань

Як встановити TypeScript у проект?

npm i -D typescript.
npx tsc --init створить tsconfig.json.
Додай скрипти: tsc, tsc -w.
Для Node: npm i -D ts-node.
Перевір: npx tsc -v.

Що таке tsconfig.json і за що він відповідає?

Це конфігурація компілятора.
target/module визначають вихідний JS.
strict включає строгі перевірки.
include/exclude керують файлами.
paths/baseUrl налаштовують алиаси.

Чим відрізняються type і interface?

interface розширюється через extends.
interface підтримує declaration merging.
type зручний для union/intersection.
Для об'єктів частіше беруть interface, але це не правило.
У реальності вибирай за задачею і стилем команди.

Що таке strict mode у TypeScript і чому його варто вмикати?

strict включає набір строгих флагів.
Ловить помилки з null/undefined і типами.
Покращує автодоповнення і рефакторинг.
Підвищує якість коду у великих проектах.
Вмикай поступово, якщо проект старий.

Що таке any і чому його варто уникати?

any вимикає перевірку типів.
Помилки переходять у runtime.
Замість any використовуйте unknown, generics або точні типи.
Для міграції можна тимчасово використовувати any, але з планом прибрати.
Можна обмежити через eslint/tsconfig.

Чим відрізняється unknown від any?

unknown безпечніше: його не можна використовувати без перевірки.
Вимагає narrowing (typeof, in, instanceof).
Підходить для зовнішніх даних (JSON).
any — 'дірка' у типах.
З unknown помилки ловляться на етапі компіляції.

Як типізувати результат JSON.parse?

JSON.parse повертає any.
Використовуй unknown і валідовуй структуру.
Створюй type guard або схему (zod).
Після валідacji — приведи до потрібного типу.
Не роби сліпий as YourType без перевірки.

Що таке type narrowing і як він працює?

Це звуження типу у гілках коду.
Використовуй typeof для примітивів.
Використовуй in/instanceof для об'єктів/класів.
Користувацькі type guards також звужують тип.
Допомагає писати код без зайвих кастів.

Як написати функцію type guard?

Функція повертає вираз виду x is T.
Внутрішньо перевіряй поля і типи.
Використовуй для unknown/union.
Тримай перевірки простими і покривай тестами.
Валідація схемою (zod) часто надійніша.

Що таке union types і де вони корисні?

Union: A | B.
Добре для обмежених наборів значень.
Часто використовують з дискримінуючим полем.
Вимагає narrowing перед доступом до специфічних полів.
Покращує виразність API.

Що таке intersection types і коли їх використовувати?

Intersection: A & B.
Об'єднує властивості кількох типів.
Зручно для композицій і міксинів.
Обережно з конфліктуючими полями.
Часто зустрічається у типах бібліотек.

Що таке literal types і навіщо вони потрібні?

Типи літералів: 'GET', 200, true.
Основи для union з конкретних значень.
Зручно для статусів, ролей, режимів.
Працюють разом з as const.
Допомагають уникнути помилок при друку.

Навіщо потрібен as const?

Робить літерали незмінними і вузькими.
Перетворює масив у readonly tuple.
Корисно для конфігів і словників.
Спрощує виведення типів.
Часто використовується з satisfies.

Що таке satisfies і чим він краще 'as'?

satisfies перевіряє відповідність типу, але зберігає вивід.
Не 'зажимає' значення як as.
Корисно для конфігів і мапінгів.
Допомагає ловити помилки ключів/полів.
Доступно в TS 4.9+.

Як типізувати об'єкт-словник з фіксованими ключами?

Використовуй Record<K, V>.
K може бути union літералів.
Для часткових ключів — Partial<Record<...>>.
Для перевірки без втрати виводу — satisfies.
Для динаміки — index signature.

Що таке index signature і коли він потрібен?

{[key: string]: Value}.
Для об'єктів з невідомими ключами.
Обмежує тип усіх значень.
Може конфліктувати з конкретними полями — обережно.
Іноді краще Map<string, V>.

Як типізувати масив з фіксованою структурою (tuple)?

Tuple: [number, string].
Можна робити readonly tuple через as const.
Для іменованих елементів: [id: string, value: number].
Зручно для пар key-value.
Для великих структур краще interface.

Що таке generics і навіщо вони потрібні?

Дозволяють параметризувати типи.
Наприклад: function first<T>(a: T[]): T.
Убирають дублювання коду.
Можна обмежувати T через extends.
Покращують типізацію бібліотек.

Як обмежувати generics через extends?

<T extends { id: string }>.
Тоді у T гарантовано є id.
Можна обмежувати union'ом.
Для ключів використовуйте extends keyof.
Обмеження покращують автодоповнення.

Як типізувати функцію getProperty(obj, key)?

<T, K extends keyof T>(obj: T, key: K) => T[K].
keyof дає набір ключів.
T[K] повертає тип значення за ключем.
Працює і з optional полями.
Це канонічний приклад generics.

Що таке keyof і чому він корисний?

keyof T дає union ключів типу.
Дозволяє безпечно індексувати.
Зручний у утилітах Pick/Omit.
Допомагає створювати типобезпечні API.
Працює разом з mapped types.

Що таке mapped types?

Це типи виду { [K in keyof T]: ... }.
Дозволяють трансформувати поля.
На них основані Partial/Required/Readonly.
Можна змінювати модифікатори: -? / +readonly.
Потужний інструмент для бібліотек.

Як працює Partial<T> і коли його використовувати?

Робить усі поля optional.
Зручно для патч-об'єктів (update).
Небезпека: втрачається гарантія наявності полів.
Часто краще Partial<Pick<T, ...>>.
Для форми введення — хороший варіант.

У чому різниця між Pick і Omit?

Pick бере підмножину полів.
Omit виключає поля.
Зручно для DTO і публічних моделей.
Уникайте дублювання структур.
Комбінуйте з Partial/Required.

Що таке Record<K, V> і де він корисний?

Record створює об'єкт з ключами K і значеннями V.
Зручно для словників і маппінгів.
K може бути union'ом.
Для неповних ключів використовуйте Partial<Record<...>>.
Для суворої перевірки ключів — satisfies.

Як типізувати функції зворотного виклику (callbacks)?

Опиши сигнатуру: type Fn = (x: number) => string.
Для подій — використовуй типи бібліотеки (React, DOM).
Для async — Promise<...>.
Не забувай про опційні параметри.
Зроби явним повернене значення.

Як типізувати Promise та async/await?

async функція повертає Promise<T>.
Вкажи тип результату: Promise<User>.
await розкриває Promise.
Обробляй помилки через try/catch.
Не повертай any з async-ланцюгів.

Що таке overloads (перевантаження) функцій?

Можна оголосити кілька сигнатур.
Реалізація одна, але типи різні.
Корисно для функцій з різними режимами.
Перевантаження мають бути сумісними.
Іноді простіше використовувати union і narrowing.

Чим відрізняється type assertion (as) від реальної перевірки типів?

as не перевіряє runtime.
Він каже компілятору: 'повір'.
Може приховати баги.
Краще спочатку валідовувати дані.
Використовуй as тільки коли точно знаєш структуру.

Що таке non-null assertion (!) і чому він небезпечний?

x! говорить, що x не null/undefined.
Компилятор перестає скаржитись.
В runtime може впасти, якщо ти помилився.
Краще перевірка if (x) або optional chaining.
Використовуй рідко і усвідомлено.

Як працює optional chaining (?.)?

Дозволяє безпечно звертатися до вкладених полів.
obj?.a?.b поверне undefined, якщо щось відсутнє.
Працює з викликом функцій: fn?.().
Не замінює бізнес-валидацію.
Зручно для UI та даних з API.

Як працює nullish coalescing (??)?

a ?? b бере b тільки якщо a null/undefined.
Не плутати з 0/''/false як ||.
Відмінно для дефолтів.
Часто комбінують з optional chaining.
Розумій різницю між || і ??.

Як типізувати помилки в catch у строгому режимі?

В TS catch-перемінна часто unknown.
Перевір: err instanceof Error.
Логуй безпечно: String(err).
Для бібліотек роби свої типи помилок.
Не передбачай структуру без перевірки.

Що таке discriminated union і як його проектувати?

Union з полем-тегом: type: 'a' | 'b'.
У switch по тегу тип автоматично звужується.
Зручно для подій і стану.
Додавай exhaustive check (never).
Це один із кращих патернів TS.

Як зробити exhaustive check у switch?

У default присвой змінну типу never.
Якщо з’явиться новий варіант union — компілятор скаржитиметься.
Приклад: const _exhaustive: never = x.
Допомагає підтримувати код.
Особливо корисно в reducer'ах.

Як типізувати класи та їх поля у TypeScript?

Вказуй модифікатори: public/private/protected.
Оголошуй поля заздалегідь або в constructor.
Використовуй readonly для незмінних.
Параметр-властивість: constructor(private api: Api) {}.
Не зловживай класами, якщо достатньо функцій.

Що таке abstract class і interface: що обрати?

interface описує контракт без реалізації.
abstract class може містити спільну реалізацію.
У TS можна реалізувати кілька interface, але успадковувати один клас.
Для 'портів' та API зазвичай достатньо interface.
Abstract зручний для базових компонентів/шаблонів.

Як типізувати this у методах і функціях?

У методів класів this виводиться автоматично.
Для функцій можна вказати this-параметр: function f(this: X) {}.
Стрілкові функції беруть this із зовнішнього контексту.
У callbacks часто втрачається this — біндуй або використовуй стрілки.
Правильна типізація ловить помилки.

Як працює enum і чому часто радять union замість enum?

enum генерує runtime-код (зазвичай).
Може ускладнити bundle.
Union літералів простіше і tree-shakable.
Якщо потрібно runtime-значення — можна enum або const object.
const enum прискорює, але небезпечний для бібліотек.

Що таке const enum і в чому його ризики?

const enum інлайниться у код.
Зменшує runtime-накладні.
Може ламатися при збірці бібліотек і різних налаштуваннях.
Не завжди сумісний з babel/transpileOnly.
Часто краще об'єкт + as const.

Як типізувати функції вищого порядку (HOF)?

Обґрунтовуй вхідний і вихідний тип.
Зберігай сигнатуру через generics.
Для декораторів функцій використовуй <T extends (...a: any[]) => any>.
Повертай Parameters<T> і ReturnType<T>.
Це дає сильну типізацію.

Що таке ReturnType і Parameters і коли вони потрібні?

ReturnType<F> бере тип результату функції.
Parameters<F> бере кортеж аргументів.
Корисно для обгорток і проксі.
Знижує дублювання.
Працює тільки з типами функцій.

Як типізувати систему подій (event emitter) безпечно?

Зроби map: EventName -> payload type.
on<K extends keyof Events>(name: K, cb: (p: Events[K]) => void).
emit аналогічно.
Так ти уникнеш помилок типів у подіях.
Гарний патерн для фронту і ноди.

Як типізувати API-клієнт і відповіді сервера?

Визнач DTO типи для відповідей.
Використовуй runtime-валидацію для зовнішніх даних.
Для fetch роби функцію request<T>() що повертає Promise<T>.
Ніколи не довіряй серверу без перевірок.
Логуй і обробляй помилки статусів.

Як типізувати fetch і обробку JSON?

const res = await fetch(url).
Перевіряй res.ok.
const data: unknown = await res.json().
Валідируй data і потім приведи до типу.
Таймаути/ретраї додавай окремо.

Як у TS правильно працювати з DOM-типами?

Включи lib: ['DOM'].
Використовуй querySelector з generic: document.querySelector<HTMLDivElement>('...').
Перевіряй на null.
Для подій використовуй MouseEvent/KeyboardEvent.
Не кастуй без необхідності.

Як типізувати addEventListener і обробники?

element.addEventListener('click', (e) => { ... }).
e має тип MouseEvent (для кліка).
Для window/document типи теж виводяться.
Якщо потрібно, уточнюй target через instanceof.
Не використовуй any для подій.

Як типізувати React-компоненти на TypeScript?

Props через interface/type.
function Component(props: Props) { ... }.
Для children: React.ReactNode.
Для подій: React.ChangeEvent<HTMLInputElement>.
Не завжди потрібен React.FC.

Коли використовувати React.FC, а коли ні?

React.FC додає children за замовчуванням.
Іноді це небажано.
Типізація defaultProps відрізняється.
Прості функції з Props зазвичай достатньо.
Обирай єдиний стиль у проекті.

Як типізувати useState у React?

useState<number>(
фіксує тип.
Для nullable: useState<User | null>(null).
Для складних об'єктів краще інтерфейс.
Лінива ініціалізація: useState(() => compute()).
Уникай any у стані.

Як типізувати useRef?

useRef<HTMLDivElement | null>(null).
Перевіряй ref.current на null.
Для mutable значення: useRef<number>(0).
Не використовуй ref як стан.
Для callbacks можна useCallback ref.

Що таке declaration merging і де воно зустрічається?

interface може об'єднуватися за ім'ям.
Зручно для розширення глобальних типів.
Часто використовується для доповнення Window/NodeJS.ProcessEnv.
Може бути джерелом конфліктів.
Контролюй, де оголошуєш глобальні типи.

Як розширити тип Window або ProcessEnv?

Створи d.ts файл у проекті.
declare global { interface Window { ... } }.
Для env: declare namespace NodeJS { interface ProcessEnv { ... } }.
Не забудь export {} у файлі.
Слідкуй, щоб include бачив цей файл.

Що таке .d.ts файли і навіщо вони потрібні?

Це файли декларацій типів.
Потрібні для JS-бібліотек без типів.
Можуть описувати API без реалізації.
Використовуються DefinitelyTyped (@types/*).
Підключаються через types/include.

Як додати типи для JS-бібліотеки без @types?

Спочатку перевір, чи немає вбудованих типів.
Якщо ні — створи мінімальний .d.ts з declare module 'lib'.
Постепенно уточнюй типи за мірою використання.
Можна написати wrapper API зі строгими типами.
Для складних випадків — генерація з OpenAPI/JSON schema.

Що таке moduleResolution і коли його змінювати?

Визначає, як TS шукає модулі.
NodeNext потрібен для ESM/Node 16+.
Bundler підходить для Vite/webpack.
Classic майже не потрібен.
Вибір залежить від рантайму та збірщика.

Як налаштувати алиаси шляхів (paths) у tsconfig?

Вкажи baseUrl.
Додай paths: { '@/*': ['src/*'] }.
Налаштуй збірщик (webpack/vite) аналогічно.
Налаштуй jest/vitest алиаси теж.
Інакше в runtime алиаси не працюватимуть.

Чому TS проходить, а в runtime модуль не знаходиться?

TS перевіряє шляхи за tsconfig paths.
Але Node/бандлер може не знати про алиаси.
Потрібно налаштувати резолвер у збірщику/тестах.
Для Node можна використовувати tsconfig-paths.
Завжди перевіряй конфігурацію runtime.

Що таке isolatedModules і коли він потрібен?

Потрібен для transpile через Babel/'один файл'.
Забороняє деякі конструкції (наприклад, const enum).
Допомагає сумісності зі збірщиками.
У Next.js зазвичай вмикають.
Краще тримати увімкненим у фронтенд-проектах.

У чому різниця між tsc і babel/swc при збірці TS?

tsc виконує типову перевірку і може емитити JS.
Babel/SWC швидко транспілюють, але не перевіряють типи.
У таких схемах типчек запускають окремо (tsc --noEmit).
Next/Vite часто використовують SWC/Babel.
Перевірка типів потрібна в CI.

Як увімкнути перевірку типів без генерації JS?

Запусти: tsc --noEmit.
Або постав noEmit: true в tsconfig.
Зручно для проектів, де збірку робить інший інструмент.
В CI роби окремий крок typecheck.
Помилки типів тоді не потраплять у прод.

Що таке source maps і як вони допомагають?

Пов’язують скомпільований JS із вихідним TS.
Спрощують відладку в браузері і Node.
Вмикається sourceMap: true.
У проді іноді відключають через розмір і безпеку.
Для Sentry корисні.

Як правильно типізувати конфіг-об’єкти?

Використовуй const + as const для літералів.
Для перевірки структури застосовуй satisfies.
Уникай any.
Зберігай конфіг у одному місці, не розмазуй.
Для env-змінних роби явне перетворення типів.

Як типізувати process.env і позбавитись 'string | undefined'?

В runtime змінні можуть відсутні.
Спершу валідируй наявність (наприклад, через функцію requireEnv).
Після перевірки можна повернути string.
Краще робити єдиний шар конфігурації.
Для строгої схеми використовуй zod/valibot.

Як типізувати функцію, яка кидає помилку (never)?

Функції, що завжди кидають/завершують процес, повертають never.
Приклад: function fail(msg: string): never { throw new Error(msg) }.
Зручно в exhaustive checks.
Допомагає аналізу потоку.
Покращує читабельність помилок.

Що таке типи шаблонних літералів?

Типи рядків із шаблоном: `user:${string}`.
Корисно для побудови ключів і маршрутів.
Працюють з union і mapped types.
Ускладнюють типи, використовуйте обережно.
Відмінно для подій/ID-шників.

Як типізувати рядки формату UUID/id?

Повної валідації типами зробити не можна.
Можна створити брендований тип: type UUID = string & { __brand: 'UUID' }.
Перетворення лише після перевірки regex.
Це підвищує безпеку API.
Без runtime перевірки бренд безглуздо.

Що таке branded types і навіщо вони потрібні?

Це спосіб розрізняти однакові примітиви (string vs Email).
Робиться через перетин із 'маркером'.
Вимагає фабрики/валідації.
Зменшує помилки при передачі параметрів.
Корисно в доменній моделі.

Як типізувати функції-валідатори?

Повертають boolean і type predicate.
Наприклад: (x: unknown): x is User.
Тримай перевірки на одному рівні, без side effects.
Валідація схемою часто простіша.
Додай тести на валідатори.

Що таке conditional types?

Типи виду T extends U ? X : Y.
Використовуються для розумних утиліт.
Можуть розподілятися по union.
Іноді сильно сповільнюють компіляцію.
Застосовуй переважно для бібліотечного коду.

Що таке infer у conditional types?

infer дозволяє 'витягнути' частину типу.
Наприклад, отримати тип елемента масиву.
Використовується в ReturnType/Parameters.
Потужно, але важче читати.
Пиши приклади і тестуй типи.

Як отримати тип елемента масиву (array element type)?

Для масиву T[] елемент — T.
Для довільного: type Elem<A> = A extends (infer T)[] ? T : never.
Для readonly масивів враховуй readonly.
Корисно в узагальнених утилітах.
Не забувай про tuple.

Що таке utility types Required/Readonly і навіщо?

Required робить поля обов’язковими.
Readonly робить поля тільки для читання.
Використовуй для публічних інтерфейсів і імутабельності.
Є ReadonlyArray і readonly tuple.
Зменшує кількість мутацій у коді.

Як типізувати partial update об’єкта безпечно?

Не роби Partial<T> на все підряд.
Обмеж набір полів: Partial<Pick<T, 'a' | 'b'>>.
Для заборони деяких полів — Omit.
Валідируй дані перед застосуванням.
Для API краще окремий DTO.

Як типізувати функцію merge без втрати типів?

<A extends object, B extends object>(a: A, b: B): A & B.
Але враховуй конфліктуючі ключі.
В runtime застосовуй Object.assign/spread.
Для глибокого merge типи складніші.
Іноді краще явний опис результату.

Як працювати з readonly і мутаціями об'єктів?

Readonly забороняє присвоювання полям.
Для змін створіть копію через {...obj}.
Для масивів: [...arr].
Використовуйте імутабельні патерни у стані.
Для глибоких структур потрібна дисципліна/immer.

Як типізувати об'єкт, який може бути null?

Використовуйте union: T | null.
Перевіряйте if (x == null) return.
Уникайте ! для обходу.
Для ланцюгового доступу використовуйте ?.
Продумайте, де саме допускається null.

Як правильно обрати між null і undefined?

undefined частіше означає 'відсутність значення' за замовчуванням.
null часто використовується як явне 'порожньо'.
Тримайте єдиний стиль у проекті.
В API краще документувати.
У strictNullChecks обидва вимагають обробки.

Як типізувати функції з опційними параметрами?

Параметр: foo(x?: string).
Або foo(x: string | undefined).
Враховуйте, що x може бути undefined.
Встановлюйте дефолт: function foo(x: string = 'a').
Не робіть багато опційних підряд — краще об'єкт параметрів.

Як типізувати 'options object' замість довгого списку аргументів?

Створіть interface Options { ... }.
function f(opts: Options) {}.
Надання дефолтів через деструктуризацію.
Це зручніше для розширення API.
Дозволяє робити Partial<Options> для дефолтів.

Як працює структурне типізування в TypeScript?

В TS важлива структура, а не ім'я типу.
Два типи сумісні, якщо поля збігаються.
Це зручно, але може приховати помилки у домені.
Branded types допомагають розрізняти.
Завжди думайте про межі системи.

Що таке номінальне типізування і як імітувати його в TS?

Номінальне — сумісність за ім'ям типу.
В TS можна імітувати через бренди.
type UserId = string & { __brand: 'UserId' }.
Створіть функцію-конструктор після валідації.
Це зменшує випадкові підстановки.

Як типізувати функції роботи з масивами (map/filter/reduce) без any?

TS автоматично виводить типи колбеків.
Слідкуйте, щоб вихідний масив був типізований.
Для reduce вказуйте початкове значення і тип акумулятора.
Уникайте reduce без initial — можуть виникнути проблеми.
Для filter іноді потрібен type guard.

Як зробити type guard для filter?

Приклад: const isDefined = <T>(x: T | undefined): x is T => x !== undefined.
Тоді arr.filter(isDefined) видалить undefined.
Аналогічно для null.
Зручно у пайплайнах даних.
Створюйте такі утиліти в одному місці.

Що таке satisfies для масивів і словників?

Можна перевірити тип масиву, не втрачаючи literal types.
Приклад: const routes = [...] satisfies Route[].
Компилятор перевірить структуру елементів.
Але збереже конкретику значень.
Гармонійний баланс між 'як є' і строгим перевіркою.

Як прискорити компіляцію TypeScript у великому проекті?

Увімкни incremental.
Використовуй project references.
Зменшуй include і виключи build/output папки.
Уникай складних conditional types у гарячих місцях.
Онови TS до актуальної версії.

Що таке project references і навіщо вони потрібні?

Це розбиття монорепо на проекти TS.
Прискорює збірку і typecheck.
Дозволяє 'інкрементально' перевіряти лише змінені частини.
Вимагає composite: true.
Добре для великих кодових баз.

Як налаштувати monorepo з TypeScript?

Використовуй менеджер workspace (pnpm/yarn/npm).
Створи базовий tsconfig і наслідуйся від нього.
Увімкни project references.
Слідкуй за єдиними версіями TS і типів.
Налаштуй лінтер і форматтер однаково.

Як правильно типізувати публічний API бібліотеки?

Експортуй лише потрібні типи.
Не експортуй внутрішні деталі.
Стабілізуй contract і версіонуй.
Генеруй d.ts (declaration: true).
Тестуй типи через dtslint або tsd.

Як перевірити типи у CI?

Додай скрипт: npm run typecheck.
Запускай tsc --noEmit.
Окремо запускай eslint/test.
Не покладайся на IDE.
Зроби перевірку обов’язковою перед merge.

Як типізувати проект Node.js на TypeScript?

Налаштуй module і moduleResolution під ESM/CJS.
Додай @types/node.
Обери раннер: ts-node, tsx або збірку через tsc.
Слідкуй за шляхами/аліасами.
Перевір запуск у проді (без ts-node).

У чому різниця між ESM і CommonJS у контексті TypeScript?

ESM використовує import/export у runtime.
CJS використовує require/module.exports.
TS може емитити обидва варіанти.
У Node важливі package.json type і розширення .mts/.cts.
Помилки частіше через змішування систем.

Як налаштувати TypeScript для Node ESM (NodeNext)?

module: NodeNext і moduleResolution: NodeNext.
У package.json постави type: module.
Використовуй розширення/exports правильно.
Для типів можна .d.ts або .mts.
Перевір збірку і запуск разом.

Як типізувати Express middleware?

Встанови @types/express.
Типи Request, Response, NextFunction.
Розширюй Request через declaration merging, якщо додаєш поля.
Робіть явні типи для body/query/params.
Валідовуй вхідні дані.

Як типізувати DTO і валідацію вхідних даних?

Типи TS не валідовують runtime.
Використовуй zod/yup/class-validator.
Схема має бути джерелом істини.
Генеруй тип із схеми, якщо бібліотека дозволяє.
Тримай валідатор на межі системи (API).

Як безпечно працювати з зовнішніми даними: JSON, API, localStorage?

Вважайте все зовнішнє як unknown.
Валідовуйте структуру (schema/type guards).
Обробляйте відсутність полів і версії.
Логуйте помилки парсингу та фолбеки.
Не використовуйте as без перевірок.

Як типізувати localStorage і перетворення типів?

localStorage зберігає рядки.
Створюйте функції getJSON<T>() з unknown + валідатором.
Для чисел/булевих — явне перетворення.
Обробляйте відсутність ключа.
Версіонізуйте формат збереження.

Як правильно типізувати помилки для доменної логіки?

Створюйте свої класи помилок (extends Error).
Додавайте код/тип причини.
Ловіть і маппіть у верхньому шарі (API/UI).
Не кидайте рядками.
Документуйте, які помилки можливі.

Які типові антипатерни в TypeScript слід уникати?

any і ланцюжки as as.
! замість перевірок.
Складні типи заради типів без вигоди.
Дублювання DTO замість утиліт Pick/Omit.
'Магія' в типах без тестів і прикладів.

Як писати підтримувані типи: практичні правила?

Спочатку прості типи, ускладнюйте за потреби.
На межах системи валідовуйте runtime.
Не приховуйте проблему через any/as.
Використовуйте discriminated unions для станів.
Підтримуйте єдиний стиль і типчекі в CI.