Ми навчилися створювати "цеглинки" (primitive types). Тепер час будувати "будинки" (complex structures). У JavaScript майже все є об'єктом. У TypeScript вміння описувати форму об'єкта — це 80% успіху.
Ми переходимо від "перевірки типів" до "моделювання домену".
Це вічна війна. "Що використовувати?". Давайте розберемося раз і назавжди.
Інтерфейс — це спосіб описати форму об'єкта. Він каже: "Я очікую об'єкт, у якого є такі поля".
interface User {
id: number
username: string
isActive: boolean
}
const admin: User = {
id: 1,
username: 'admin',
isActive: true,
}
Це унікальна фіча інтерфейсів. Якщо ви оголосите два інтерфейси з одним іменем, вони зіллються в один.
// Файл 1 (або бібліотека)
interface Settings {
theme: 'dark' | 'light'
}
// Файл 2 (ваше розширення)
interface Settings {
fontSize: number
}
// Результат: Settings тепер має ОБИДВА поля
const config: Settings = {
theme: 'dark',
fontSize: 14,
}
Це критично важливо для розширення бібліотек (наприклад, додавання полів до Window або Request в Express).
type — це просто інша назва для будь-якого типу. Це може бути об'єкт, примітив, функція або Union.
// 1. Примітив
type ID = string
// 2. Union (Об'єднання)
type Status = 'loading' | 'success' | 'error'
// 3. Функція
type Handler = (event: Event) => void
// 4. Об'єкт (схоже на Interface)
type UserType = {
id: ID
status: Status
}
type Settings двічі — отримаєте помилку Duplicate identifier.| Характеристика | Interface | Type Alias |
|---|---|---|
| Об'єкти | ✅ Так | ✅ Так |
| Примітиви | ❌ Ні | ✅ Так |
| Union Types | ❌ Ні | ✅ Так |
| Declaration Merging | ✅ Так (для бібліотек) | ❌ Ні |
implements в класах | ✅ Так | ✅ Так (обмежено) |
interface для опису об'єктів та публічних API (щоб їх могли розширювати інші).
Використовуйте type для Union Types, функцій, примітивів та складних маніпуляцій з типами.
Якщо сумніваєтесь — починайте з interface.Описувати прості поля нудно. Давайте глянемо на реальні сценарії.
Іноді поле може бути, а може й ні.
interface Product {
name: string
description?: string // string | undefined
}
// ✅ Обидва варіанти валідні
const p1: Product = { name: 'iPhone' }
const p2: Product = { name: 'Samsung', description: 'Android phone' }
// ⚠️ Доступ до optional поля
// console.log(p1.description.toUpperCase()); // Error: Object is possibly 'undefined'.
console.log(p1.description?.toUpperCase()) // Optional Chaining to the rescue!
Імутабельність — друг стабільності.
interface UserProfile {
readonly id: number // Не можна змінити після створення
name: string
}
const user: UserProfile = { id: 101, name: 'Alice' }
user.name = 'Bob' // ✅ ОК
// user.id = 102; // 🛑 Error: Cannot assign to 'id' because it is a read-only property.
Це критично важливий момент. Модифікатор readonly у TypeScript працює лише на першому рівні вкладеності.
Якщо властивість об'єкта сама є об'єктом, то TypeScript заборонить вам замінити цей вкладений об'єкт цілком, але дозволить змінювати його внутрішні поля.
interface Address {
city: string
street: string
}
interface UserProfile {
readonly id: number
readonly address: Address // Весь об'єкт address "readonly"
name: string
}
const user: UserProfile = {
id: 1,
name: 'Alice',
address: {
city: 'Kyiv',
street: 'Khreschatyk',
},
}
// 🛑 Помилка: id тільки для читання
// user.id = 2;
// 🛑 Помилка: ми не можемо ПЕРЕЗАПИСАТИ весь об'єкт address
// user.address = { city: 'Lviv', street: 'Rynok' };
// ✅ ОК: Але ми МОЖЕМО змінити поле всередині об'єкта address!
user.address.city = 'Lviv'
address зберігає посилання на об'єкт. Ви не можете змінити посилання (присвоїти новий об'єкт), але сам об'єкт у пам'яті залишається мутабельним.Якщо ви хочете повної, глибокої незмінності, вам потрібно або позначати кожне вкладене поле як readonly, або використовувати утиліту Readonly<T> (про яку ми поговоримо в розділі про Generics).
Що робити, якщо ми не знаємо назв полів заздалегідь? Наприклад, кеш або словник.
interface Cache {
[key: string]: any // Ключ - рядок, значення - будь-що
}
const memory: Cache = {}
memory['user_1'] = 'Alice'
memory['config'] = { theme: 'dark' }
// Але це небезпечно! TS не перевіряє доступ.
// const item = memory["non_existent"]; // Typs: any. Runtime: undefined.
Краще використовувати Record<K, V> (про це пізніше).
Це "Святий Грааль" моделювання станів у TypeScript.
Union означає "АБО". Змінна може бути або типом A, або типом B.
type ID = string | number
function printId(id: ID) {
console.log(`Your ID is: ${id}`)
// 🛑 Error: Property 'toUpperCase' does not exist on type 'string | number'.
// id.toUpperCase();
// ✅ Type Narrowing (Звуження типу)
if (typeof id === 'string') {
console.log(id.toUpperCase()) // Тут id - це точно string
} else {
console.log(id.toFixed(2)) // Тут id - це точно number
}
}
interface Bird {
fly(): void
layEggs(): void
}
interface Fish {
swim(): void
layEggs(): void
}
function getSmallPet(): Bird | Fish {
/* ... */
}
const pet = getSmallPet()
pet.layEggs() // ✅ Є у обох
// pet.fly(); // 🛑 Error: Fish does not fly!
Intersection означає "І". Ми склеюємо типи разом.
interface HasName {
name: string
}
interface HasAge {
age: number
}
type Person = HasName & HasAge
const bob: Person = {
name: 'Bob',
age: 30,
// Якщо забути хоч одне поле - буде помилка
}
Це часто використовується для композиції.
type APIResponse = {
data: User[]
page: number
}
type ErrorResponse = {
error: string
code: number
}
// Це навряд чи має сенс (відповідь не може бути одночасно успішною І поламаною)
type Wtf = APIResponse & ErrorResponse
Це "коронна фіча" системи типів TypeScript. Якщо ви зрозумієте цей патерн, якість вашої архітектури зросте вдесятеро.
Уявіть, що ви описуєте фігури:
interface Shape {
kind: string
radius?: number // Тільки для кола
sideLength?: number // Тільки для квадрата
}
Це погано, бо TS не знає, що radius існує тільки тоді, коли kind === "circle". Вам доведеться постійно використовувати !, заводити перевірки на undefined або робити as.
Щоб створити Дискриміноване Об'єднання, потрібні три інгредієнти:
type, status або kind).Давайте перепишемо фігури:
interface Circle {
kind: 'circle' // Дискримінант
radius: number
}
interface Square {
kind: 'square' // Дискримінант
sideLength: number
}
interface Rectangle {
kind: 'rectangle'
width: number
height: number
}
type Shape = Circle | Square | Rectangle
Тепер TypeScript розуміє взаємозв'язок між значенням kind та наявністю інших полів.
function getArea(shape: Shape) {
switch (shape.kind) {
case 'circle':
// Тут shape — це точно Circle. Радіус доступний!
return Math.PI * shape.radius ** 2
case 'square':
// Тут shape — це точно Square. sideLength доступний!
return shape.sideLength ** 2
case 'rectangle':
return shape.width * shape.height
}
}
Це одна з найбільш недооцінених фішок. Що, якщо ми додамо новий тип фігури Triangle, але забудемо оновити функцію getArea?
Ми можемо змусити TypeScript сваритися на нас за допомогою типу never.
function getArea(shape: Shape) {
switch (shape.kind) {
case 'circle':
return Math.PI * shape.radius ** 2
case 'square':
return shape.sideLength ** 2
case 'rectangle':
return shape.width * shape.height
default:
// Якщо ми обробили всі типи, то сюди ми ніколи не потрапимо.
// Отже, тип shape тут має бути 'never'.
const _exhaustiveCheck: never = shape
return _exhaustiveCheck
}
}
Якщо ви додасте Triangle у type Shape, але не додасте case "triangle" у switch, TypeScript видасть помилку:
Type 'Triangle' is not assignable to type 'never'.
Це гарантує, що ви ніколи не забудете обробити новий стан системи.
Це найчастіший сценарій у реальній розробці.
type ApiResponse<T> =
| { status: 'loading' }
| { status: 'success'; data: T; timestamp: number }
| { status: 'error'; error: Error; code: number }
function handleResponse(res: ApiResponse<string[]>) {
if (res.status === 'success') {
console.log('Дані отримано:', res.data) // OK
// console.log(res.error); // Error: Поля error не існує в успішній відповіді
}
}
Цей приклад ідеально демонструє, як Дискриміновані Об'єднання допомагають уникати логічних помилок при рендерингу.
// 1. Описуємо кожен стан окремо для чистоти
interface LoadingState {
status: 'loading'
}
interface SuccessState {
status: 'success'
data: string[]
}
interface ErrorState {
status: 'error'
message: string
}
// 2. Створюємо Union
type UIState = LoadingState | SuccessState | ErrorState
// 3. Функція рендерингу, яка ніколи не помилиться
function renderPage(state: UIState) {
if (state.status === 'loading') {
return 'Завантаження... 🔄'
}
if (state.status === 'error') {
// Тут TypeScript знає, що є message, але НЕМАЄ data
return `Помилка: ${state.message} ❌`
}
if (state.status === 'success') {
// Тут TypeScript знає, що є data, але НЕМАЄ message
return `Дані: ${state.data.join(', ')} ✅`
}
}
isLoading, isError, data як окремі стейти (наприклад, у React useState), ви можете випадково отримати стан isLoading: true ТА data: [...] одночасно. Дискриміноване об'єднання робить такі "неможливі стани" неможливими на рівні типів.Ми вже бачили їх вище ("loading", "success").
Це типи, які допускають лише конкретне значення, а не весь клас значень.
// String Literal
type Direction = 'North' | 'South' | 'East' | 'West'
function move(dir: Direction) {}
move('North') // ✅
// move("Up"); // 🛑 Error
// Numeric Literal
type DiceRoll = 1 | 2 | 3 | 4 | 5 | 6
// Boolean Literal
type IsEnabled = true // Змінна може бути ТІЛЬКИ true
Ви можете комбінувати літерали.
type Color = 'red' | 'blue'
type Variant = 'light' | 'dark'
// Автоматична генерація всіх комбінацій:
// "red-light" | "red-dark" | "blue-light" | "blue-dark"
type ThemeClass = `${Color}-${Variant}`
Це використовується в дизайн-системах.
interface ButtonProps {
size: 'sm' | 'md' | 'lg'
variant: 'primary' | 'secondary' | 'danger'
}
// 9 можливих комбінацій
У більшості мов (C#, Java, Swift) перерахування (Enums) — це база. У TypeScript (enum) — це одна з небагатьох фіч, яка додає реальний код у JavaScript під час компіляції. І саме тому навколо них стільки суперечок.
Enum (Enumeration) — це набір іменованих констант. Він допомагає замінити "магічні рядки" або "магічні числа" на зрозумілі імена, групуючи їх в одну сутність.
Кейси використання:
ADMIN, USER).PENDING, SHIPPED).UP, DOWN, LEFT, RIGHT).Це варіант за замовчуванням. Якщо не вказувати значення, TS почне з 0 і буде інкрементувати їх.
enum UserRole {
Admin, // 0
Editor, // 1
User, // 2
}
const myRole: UserRole = UserRole.Admin // 0
До версії TS 5.0 числові енуми мали "дірку" в безпеці: ви могли присвоїти змінній типу UserRole будь-яке число, навіть якщо його немає в списку.
const role: UserRole = 123 // ⚠️ Жодних помилок від компілятора!
Це ламає саму ідею типізації, бо функція, яка очікує UserRole, може отримати 123 і впасти під час виконання.
Вони з'явилися пізніше, щоб зробити код більш читабельним під час налагодження (debug).
enum OrderStatus {
Pending = 'PENDING',
Shipped = 'SHIPPED',
Delivered = 'DELIVERED',
}
На відміну від інтерфейсів чи типів, які зникають після компіляції, enum перетворюється на об'єкт.
Код TS:
enum Color {
Red = 'red',
}
Код JS після компіляції:
var Color
;(function (Color) {
Color['Red'] = 'red'
})(Color || (Color = {}))
Це "IIFE" (Immediately Invoked Function Expression). Проблема в тому, що сучасні збирачі проектів (Vite, Webpack) часто не можуть видалити цей код, навіть якщо ви його не використовуєте (проблема Tree-shaking). Це робить ваш фінальний JS-файл важчим.
as const ObjectЦе підхід, який використовують професійні команди. Ми беремо звичайний об'єкт і "заморожуємо" його типи.
const UserRole = {
Admin: 'admin',
Editor: 'editor',
User: 'user',
} as const // 👈 Магія тут!
Що робить as const?
readonly.string на Literal Types ("admin", "editor", "user").keyof typeofЦя конструкція часто лякає новачків. Давайте розберемо її по кроках:
type UserRole = (typeof UserRole)[keyof typeof UserRole]
typeof UserRole: Отримує тип самого об'єкта.
{ readonly Admin: "admin", readonly Editor: "editor", ... }keyof typeof UserRole: Бере всі ключі цього типу.
"Admin" | "Editor" | "User"Object[Keys]: Каже: "Дай мені типи всіх значень, які доступні за цими ключами".
"admin" | "editor" | "user"Тепер у нас є тип, який поводиться як enum, але є чистим JS і ідеально підтримує Tree-shaking.
as constconst API_ENDPOINTS = {
GET_USERS: '/api/v1/users',
GET_POSTS: '/api/v1/posts',
LOGIN: '/api/auth/login',
} as const
type ApiEndpoint = (typeof API_ENDPOINTS)[keyof typeof API_ENDPOINTS]
function fetchData(url: ApiEndpoint) {
/* ... */
}
fetchData(API_ENDPOINTS.LOGIN) // ✅
fetchData('/api/hack') // 🛑 Помилка!
const HttpStatus = {
Ok: 200,
NotFound: 404,
ServerError: 500,
} as const
type HttpStatus = (typeof HttpStatus)[keyof typeof HttpStatus] // 200 | 404 | 500
| Функція | enum | as const Object |
|---|---|---|
| Чистий JS | ❌ Ні (додає код) | ✅ Так |
| Безпека чисел | ❌ Погана | ✅ Відмінна |
| Tree-shaking | ❌ Важко | ✅ Легко |
| Складність синтаксису | ✅ Просто | ⚠️ Потрібно звикнути |
Порада: Якщо ви пишете сучасний веб-додаток — використовуйте as const об'єкти. Це стандарт галузі. Використовуйте enum тільки якщо працюєте в старому проекті, де вони вже всюди.
Як навчити TS розуміти нашу логіку перевірок?
Ми використовували typeof та instanceof. Але для складних об'єктів цього мало.
is)Це функція, яка повертає boolean, але має особливий тип повернення arg is Type.
interface Fish {
swim: () => void
}
interface Bird {
fly: () => void
}
// Функція-предикат
function isFish(pet: Fish | Bird): pet is Fish {
// Перевіряємо наявність унікального методу
return (pet as Fish).swim !== undefined
}
const myPet = getPet()
if (isFish(myPet)) {
myPet.swim() // ✅ Тут TS знає: це Fish
} else {
myPet.fly() // ✅ Тут TS знає: це точно Bird (бо не Fish)
}
asserts)Це функції, які кидають помилку, якщо умова не виконується. Корисно для тестів або валідації вхідних даних.
function assertIsString(val: any): asserts val is string {
if (typeof val !== 'string') {
throw new AssertionError('Not a string!')
}
}
const inputValue: unkown = 'hello'
assertIsString(inputValue)
// Після цього рядка inputValue вважається string
console.log(inputValue.toUpperCase())
Можна визначати типи, які посилаються самі на себе. Класичний приклад: JSON.
type JSONValue =
| string
| number
| boolean
| null
| JSONValue[] // Рекурсія: масив JSONValue
| { [key: string]: JSONValue } // Рекурсія: об'єкт з JSONValue
Це дозволяє описувати дерева будь-якої глибини.
Уявіть, що типи — це дані. Тоді Mapped Types — це функція Array.prototype.map(), але для об'єктних типів. Вони дозволяють створити новий тип, трансформуючи властивості існуючого.
Це один з найпотужніших інструментів для дотримання принципу DRY (Don't Repeat Yourself) у типах.
Якщо у JavaScript ми можемо пройтися циклом по ключах об'єкта, то у TypeScript Mapped Types роблять те саме на рівні компіляції:
const user = { name: 'Alice', age: 25 }
// JS: Перетворити всі значення на рядки
const stringifiedUser = Object.keys(user).reduce((acc, key) => {
acc[key] = String(user[key])
return acc
}, {})
Давайте розберемо цей "страшний" рядок: [Key in keyof T]: T[Key].
interface User {
id: number
name: string
}
type MappedUser = {
[K in keyof User]: User[K]
}
keyof User: Це оператор, який повертає Union тип усіх ключів.
"id" | "name".in: Аналогічно до циклу for...in. Він каже TypeScript: "пройдися по кожному значенню з цього Union'у".[K in ...]: Тут створюється змінна циклу K. На кожній ітерації K буде приймати нове значення ключа.User[K] (Indexed Access Type): Ми кажемо: "візьми тип, який відповідає ключу K в оригінальному інтерфейсі User".
K = "id", User["id"] поверне number.K = "name", User["name"] поверне string.Результат: MappedUser буде ідентичним User. Поки що ми нічого не змінили, лише скопіювали. Справжня магія починається з модифікаторів.
Ми можемо додавати або видаляти опціональність (?) та незмінність (readonly).
Додаємо ? після закриваючої дужки ключа:
type PartialUser = {
[K in keyof User]?: User[K] // Додає ? до кожного ключа
}
// Результат: { id?: number; name?: string; }
Додаємо префікс readonly:
type ReadonlyUser = {
readonly [K in keyof User]: User[K]
}
- та +)Ми можемо не тільки додавати, а й примусово видаляти префікси. Найчастіше використовується -? (зробити обов'язковим) та -readonly (зробити мутабельним).
interface PartialSettings {
theme?: string
fontSize?: number
}
// Перетворюємо всі опціональні поля на обов'язкові
type RequiredSettings = {
[K in keyof PartialSettings]-?: PartialSettings[K]
}
// Результат: { theme: string; fontSize: number; }
Уявіть, що у вас є конфігурація фіч, де значення — це або boolean, або складна конфігурація. Вам потрібен тип, де всі ці ключі існують, але кожне значення перетворене на просту функцію "Toggle".
interface Features {
darkMode: boolean
betaNav: { items: string[] }
experimentalEditor: boolean
}
type FeatureToggles = {
[K in keyof Features]: () => void
}
const toggles: FeatureToggles = {
darkMode: () => console.log('Dark Mode toggled'),
betaNav: () => console.log('Beta Nav toggled'),
experimentalEditor: () => console.log('Editor toggled'),
}
Mapped Types — це фундамент для вбудованих утиліт TypeScript, таких як Partial<T>, Required<T>, Pick<T, K> та Record<K, T>. Ми розберемо їх у наступному розділі про Generics.
keyof та typeofІноді нам треба отримати тип ключів об'єкта.
interface User {
id: number
name: string
}
// "id" | "name"
type UserKeys = keyof User
А якщо у нас є значення (об'єкт), а ми хочемо його тип? typeof.
const config = {
apiKey: '123',
timeout: 5000,
}
type Config = typeof config
// { apiKey: string; timeout: number; }
keyof typeofЦе "супер-комбо", яке розв'язує одну з найпоширеніших проблем: як отримати типи з існуючих JavaScript даних, не дублюючи їх вручну.
Уявіть, що у вас є словник конфігурації. Ви хочете написати функцію, яка приймає лише ключі цього словника.
const ThemeColors = {
primary: '#0070f3',
secondary: '#1e1e1e',
success: '#0070f3',
} as const
Якщо ви напишете function getColor(name: string), ви втратите типобезпеку (можна передати будь-який рядок). Якщо ви створите type ColorName = "primary" | "secondary" | "success", вам доведеться оновлювати цей тип щоразу, коли ви додаєте колір у об'єкт.
keyof typeofДавайте розберемо, як це працює, крок за кроком:
type ColorName = keyof typeof ThemeColors
typeof ThemeColors: Оператор typeof у контексті типів каже TypeScript: "Подивися на цю JavaScript-змінну і згенеруй тип, який точно описує її структуру".
{ readonly primary: "#0070f3", readonly secondary: "#1e1e1e", ... }keyof ...: Тепер ми беремо отриманий тип і кажемо: "Дай мені Union всіх ключів цього типу".
"primary" | "secondary" | "success"Тепер ваше джерело істини (Single Source of Truth) — це сам об'єкт ThemeColors. Додайте новий колір у об'єкт — і тип ColorName оновиться автоматично!
Ви хочете написати функцію, яка дістає значення з конфігурації за ключем.
const AppConfig = {
apiUrl: 'https://api.example.com',
retryCount: 3,
debugMode: true,
} as const
// 1. Створюємо тип ключів
type ConfigKey = keyof typeof AppConfig
// 2. Використовуємо його у функції
function getConfigValue(key: ConfigKey) {
return AppConfig[key]
}
getConfigValue('apiUrl') // ✅ Працює, повертає рядок
getConfigValue('retryCount') // ✅ Працює, повертає 3
// getConfigValue("port"); // 🛑 Помилка! Ключа "port" не існує в AppConfig
as const!
Без as const TypeScript сприймає значення об'єкта як загальні типи (string, number). as const робить їх Literals, а сам об'єкт — readonly, що є критичним для точної генерації типів.::List, який може рендерити будь-що: користувачів, товари, котів.
Вам потрібен Generic Interface.// T — це змінна типу (можна назвати як завгодно, але T стандарт)
interface ListProps<T> {
items: T[]
renderItem: (item: T) => string
}
// Використання
const users: ListProps<User> = {
items: [{ id: 1, name: 'Alice' }],
renderItem: (u) => u.name,
}
const products: ListProps<Product> = {
items: [{ name: 'iPhone' }],
renderItem: (p) => p.name,
}
type EventName = 'click' | 'hover' | 'focus'
// Генерований тип: "onClick" | "onHover" | "onFocus"
// Capitalize - вбудований утилітний тип (Intrinsic String Manipulation)
type HandlerName = `on${Capitalize<EventName>}`
const handlers: Record<HandlerName, () => void> = {
onClick: () => {},
onHover: () => {},
onFocus: () => {},
}
type Size = 'sm' | 'md' | 'lg'
type MarginClass = `m-${Size}` // "m-sm" | "m-md" | "m-lg"
type PaddingClass = `p-${Size}` // "p-sm" | "p-md" | "p-lg"
filter)null, але TS все одно думає, що там може бути null.const values = ['A', null, 'B', undefined, 'C']
// Тип result: (string | null | undefined)[] 😱
// TS не аналізує логіку колбека filter так глибоко
const result = values.filter((v) => v !== null && v !== undefined)
// result[0].toUpperCase(); // Error: Object is possibly 'null'.
// Функція, яка каже: "Якщо це true, то val точно типу T"
function isDefined<T>(val: T | null | undefined): val is T {
return val !== null && val !== undefined
}
// Тепер result: string[] ✅
const safeResult = values.filter(isDefined)
// 1. Loading
type Loading = { status: 'loading' }
// 2. Success
type Success<T> = { status: 'success'; data: T }
// 3. Error
type Failed = { status: 'error'; error: Error }
// 4. Union
type RequestState<T> = Loading | Success<T> | Failed
// Компонент (умовний)
function UserProfile({ state }: { state: RequestState<User> }) {
// 🛑 Не можна: state.data
switch (state.status) {
case 'loading':
return 'Spinner...'
case 'error':
return `Error: ${state.error.message}` // TS знає, що тут є error
case 'success':
return `User: ${state.data.username}` // TS знає, що тут є data
}
}
IUser, IService.
У TypeScript це анти-патерн.
Чому? Тому що в TS важлива структура, а не ім'я. Ви (як споживач) не повинні знати, чи це interface, чи type, чи class.// ❌ Погано
interface IUser {
name: string
}
// ✅ Добре
interface User {
name: string
}
// ❌ Погано
interface User {
username: string
jwtToken: string
shippingAddress: string
billingAddress: string
isModalOpen: boolean // UI стан в доменній моделі
}
// ✅ Добре
interface User {
username: string
}
interface AuthInfo {
token: string
}
interface Addresses {
shipping: string
billing: string
}
boolean (увімкнено/вимкнено)string (варіант A/B тесту)json (складна конфігурація)FeatureFlag як Discriminated Union.type FeatureFlag = { kind: 'boolean'; value: boolean } | { kind: 'string'; value: 'A' | 'B' }
// додумайте json
getFeatureValue(flag: FeatureFlag), яка повертає значення у правильному типі. (Спробуйте використати перевантаження функцій або Generics, але поки можна просто через перевірку kind).Config, де ключі — це назви фіч, а значення — це FeatureFlag.
Але! Зробіть так, щоб не можна було змінювати конфіг (Readonly).| Задача | Рішення | Приклад |
|---|---|---|
| Об'єднати варіанти | Union | type ID = string | number |
| Об'єднати властивості | Intersection | type User = Person & Worker |
| Опціональне поле | ? | age?: number |
| Тільки для читання | readonly | readonly id: number |
| Динамічні ключі | Index Signature | [key: string]: number |
| Ключі з типу | keyof | keyof User |
| Тип зі змінної | typeof | typeof config |
| Тип із рядка | Literal Type | "success" | "error" |
| Перевірка типу | Type Guard | arg is User |
useReducer).const LOGIN_SUCCESS = 'LOGIN_SUCCESS'Це не давало нам зв'язку між типом екшена і його пейлоадом (payload).// 1. Описуємо кожен екшен окремо
type LoginStart = { type: 'LOGIN_START' }
type LoginSuccess = { type: 'LOGIN_SUCCESS'; payload: { token: string } }
type LoginFailure = { type: 'LOGIN_FAILURE'; payload: { error: string } }
type Logout = { type: 'LOGOUT' }
// 2. Union всіх можливих екшенів
type AuthActions = LoginStart | LoginSuccess | LoginFailure | Logout
// 3. State
interface AuthState {
user: { token: string } | null
isLoading: boolean
error: string | null
}
// 4. Reducer
function authReducer(state: AuthState, action: AuthActions): AuthState {
switch (action.type) {
case 'LOGIN_START':
return { ...state, isLoading: true, error: null }
case 'LOGIN_SUCCESS':
// TS знає: тут action має поле payload з token
return { ...state, isLoading: false, user: action.payload }
case 'LOGIN_FAILURE':
return { ...state, isLoading: false, error: action.payload.error }
case 'LOGOUT':
return { ...state, user: null }
default:
// Exhaustiveness Check
const _: never = action
return state
}
}
action.type. Ви не можете звернутися до payload, якщо його немає.type ValidationResult = { isValid: true } | { isValid: false; errors: string[] }
type FormField<T> = {
value: T
isDirty: boolean
isTouched: boolean
validation: ValidationResult
}
// Використання
const emailField: FormField<string> = {
value: 'test@',
isDirty: true,
isTouched: true,
validation: { isValid: false, errors: ['Invalid email'] },
}
function renderError(field: FormField<string>) {
// Якщо валідно - нічого не рендеримо
if (field.validation.isValid) {
return null
}
// Якщо не валідно - TS знає, що є errors
return field.validation.errors.join(', ')
}
ValidationResult) в інший тип. Це композиція типів у дії.<a>) або кнопкою (<button>).type ButtonProps = {
variant: "primary" | "secondary";
children: string;
} & (
| { as: "button"; onClick: () => void; href?: never }
| { as: "a"; href: string; onClick?: never } // Якщо 'a', то onClick заборонено (для прикладу)
);
function Button(props: ButtonProps) {
if (props.as === "a") {
return <a href={props.href} className={props.variant}>{props.children}</a>;
}
return <button onClick={props.onClick} className={props.variant}>{props.children}</button>;
}
// ✅
// <Button as="button" variant="primary" onClick={() => {}}>Click</Button>
// ✅
// <Button as="a" variant="secondary" href="/home">Link</Button>
// 🛑 Error: Property 'href' does not exist on type '{ as: "button"... }'
// <Button as="button" href="/home">Bad</Button>
satisfies (TS 4.9+)satisfies:type Color = string | { r: number; g: number; b: number }
const myColor: Color = 'red'
// console.log(myColor.toUpperCase()); // 🛑 Error: Property 'toUpperCase' does not exist on type 'Color'.
satisfies:const myColor = 'red' satisfies Color
console.log(myColor.toUpperCase()) // ✅ Працює!
"red" підходить під Color, але залишив тип myColor як "red", а не Color. Це дозволяє уникати зайвих звужень типів.interface User {
name: string
}
// 🛑 Випадок 1: Object Literal (Помилка!)
const u1: User = { name: 'Alice', age: 30 } // Error: Object literal may only specify known properties.
// ✅ Випадок 2: Reference (ОК!)
const tmp = { name: 'Alice', age: 30 }
const u2: User = tmp // Працює через Структурну Типізацію.
Client з полями id, name, email."lead" (тільки email)"active" (має contractId)"inactive" (має reason чому пішов)CRMAction для Redux-подібного редюсера (actions: ADD_CLIENT, UPDATE_STATUS, DELETE_CLIENT).type?type NewType = OldType & { newField: string }.type чи interface?interface. Для всього іншого — type.readonly дозволяє змінювати вкладені об'єкти?