Запропонувати правкуПокращити цю статтюДопрацюйте відповідь до «Структурна типізація (утка типізація) в TypeScript». Ваші зміни проходять модерацію перед публікацією.Потрібне підтвердженняКонтентЩо ви змінюєте🇺🇸EN🇺🇦UAПереглядЗаголовок (UA)Коротка відповідь (UA)**Структурна типізація** в TypeScript означає сумісність типів за формою, а не за іменем. Якщо об'єкт має потрібні властивості з правильними типами, він підходить без жодного `implements`. ```typescript interface HasX { x: number; } const obj = { x: 10, extra: "ignored" }; // без implements function useX(item: HasX) { return item.x * 2; } useX(obj); // 20 ✅ ``` **Головне:** збігу форми достатньо. Для двох структурно однакових типів з різним змістом використовуй брендовані типи (branded types).Показується над повною відповіддю для швидкого нагадування.Відповідь (UA)Зображення**Структурна типізація** в TypeScript перевіряє форму об'єкта (властивості та їхні типи), а не ім'я класу чи декларацію. Якщо об'єкт має все, що вимагає тип, він підходить. ## Теорія ### TL;DR - Збіг форми важливіший за збіг імені. Об'єкт з `x: number` задовольняє `interface HasX { x: number }` без жодного `implements` - Зайві властивості в об'єкті переданому через змінну? Нормально. Зайві властивості в літералі об'єкта? Помилка - TypeScript перевіряє структуру лише під час компіляції. Жодних витрат під час виконання - Java/C# перевіряють імена, TypeScript перевіряє структуру. Ось і вся різниця - Коли дві однакові форми представляють різні речі (валюти, ID), використовуй брендовані типи (branded types) ### Швидкий приклад ```typescript interface HasX { x: number; } const point = { x: 10, extra: "ignored" }; // без "implements HasX" function useX(item: HasX): number { return item.x * 2; } console.log(useX(point)); // 20 ✅ форма збігається ``` У `point` є зайва властивість, але TypeScript перевіряє лише наявність `x: number`. Вона є, тому виклик компілюється. Зайва властивість просто ігнорується. ### Головна різниця: форма vs. ім'я У Java або C# два класи з однаковими методами залишаються різними типами, якщо один не оголошує `implements` або `extends`. TypeScript не дивиться на імена. Якщо потрібні властивості є з правильними типами, типи сумісні. ```typescript interface Cat { meow(): void; } interface CatLike { meow(): void; } const cat: Cat = { meow() {} }; const catLike: CatLike = cat; // ✅ однакова структура, сумісні ``` У номінальній системі це б не спрацювало. `Cat` і `CatLike` мають різні імена, тому вони різні типи незалежно від вмісту. ### Коли використовувати - Сторонні інтеграції: приймай будь-який об'єкт потрібної форми без обгорткових класів - React props: будь-який компонент що передає правильну форму підходить, без явного наслідування - Express middleware: власні розширення `req` приймаються, бо форма зростає структурно - Уникай коли форми мають залишатись семантично різними. Валюти, ID користувача, ключі сутностей: тут потрібні брендовані типи ### Структурна vs. номінальна типізація | Аспект | Структурна (TypeScript) | Номінальна (Java/C#) | |---|---|---| | Основа сумісності | Збіг форми | Ім'я + явна декларація | | Зайві властивості | Допустимі через змінну | Не застосовно | | Ключове слово `implements` | Необов'язкове | Обов'язкове | | Стиль помилок | Drift форми може накопичуватись непомітно | Явна невідповідність декларацій | | Найкраще для | API що змінюються, бібліотеки | Суворі контракти, фінансові системи | ### Перевірка надмірних властивостей В структурній типізації є одне спеціальне правило. Літерали об'єктів при прямому присвоєнні типізованій змінній проходять **перевірку надмірних властивостей** (excess property checking). Передаєш той самий об'єкт через змінну і перевірка зникає. ```typescript interface Config { host: string; port: number; } // ❌ прямий літерал - помилка зайвої властивості const cfg: Config = { host: "localhost", port: 3000, debug: true }; // Error: 'debug' does not exist in type 'Config' // ✅ через змінну - перевірки немає const obj = { host: "localhost", port: 3000, debug: true }; const cfg2: Config = obj; // Працює ``` Ця асиметрія часто збиває з пантелику. Обидва випадки структурні, але перший отримує додаткову увагу, бо ти передаєш TypeScript свіжий об'єкт про який він знає все. ### Як це обробляє компілятор Компілятор TypeScript (`tsc`) виконує структурну перевірку при присвоєнні типів: кожна обов'язкова властивість цільового типу повинна бути в джерелі з сумісним типом. Жодних витрат часу виконання. Типи видаляються до запуску JavaScript, тому Node.js і браузер їх не бачать. Екземпляри класів підпадають під те саме правило. Мають значення лише публічні члени: ```typescript class Animal { name: string; constructor(n: string) { this.name = n; } } class Person { name: string; constructor(n: string) { this.name = n; } } let a: Animal = new Person("Alice"); // ✅ однакова публічна форма ``` Додай `private` члени і ситуація змінюється. Приватні поля кожного класу унікальні для нього. Два класи з приватними полями більше не є структурно сумісними навіть якщо виглядають ідентично. ### Типові помилки **Помилка 1: Вважати що drift форми нешкідливий** ```typescript interface Point { x: number; y: number; } const p = { x: 1, y: 2, z: 3 }; const p2: Point = p; // ✅ компілюється function move(pt: Point) { // pt.z доступний під час виконання, але TypeScript нічого не знає про нього console.log((pt as any).z); // типова безпека тут втрачається } ``` Присвоєння компілюється, але ти втрачаєш доступ до `.z` через тип. Виправлення: index signature або явне розширення інтерфейсу. **Помилка 2: Присвоєння вужчого типу ширшому** ```typescript type A = { a: number }; type B = { a: number; b: string }; const narrow: A = { a: 1 }; const wide: B = narrow; // ❌ Відсутня властивість 'b' ``` У `A` немає `b`, тому він не може задовольнити `B`. Зворотне завжди працює: `B` підходить туди де очікується `A`. **Помилка 3: Приватні члени блокують структурну сумісність** ```typescript class Dog { private breed: string; constructor(b: string) { this.breed = b; } bark() {} } class Cat { private breed: string; constructor(b: string) { this.breed = b; } bark() {} } const d: Dog = new Cat("Persian"); // ❌ невідповідність приватних полів ``` Поля `private breed` належать різним класам. TypeScript блокує це навіть якщо класи виглядають ідентично. **Помилка 4: Покладатись на структурну типізацію для семантичної безпеки** ```typescript type USD = number; type EUR = number; function convertToEUR(amount: USD): EUR { return amount * 0.85; } const euros: EUR = 100; convertToEUR(euros); // ✅ немає помилки, але логічно неправильно ``` Я бачив цей патерн у фінансовому коді, де два числових аліаси приймали одне одного місяцями поки хтось не помітив. Обидва типи є просто `number`, тому TypeScript не може їх розрізнити. Брендовані типи вирішують це. ### Де зустрічається в реальних проектах - React: props-інтерфейси приймають будь-який об'єкт з правильною формою, явний клас не потрібен - Express: middleware отримує `(req: Request, res: Response)`; власні властивості `req` підтримуються якщо розширюєш тип структурно - Redux: action creators повертають `{ type: string; payload?: any }`; будь-який відповідний об'єкт можна передати до `dispatch` - Node.js: stream-подібні об'єкти підключаються до API без формального `implements` ### Питання на співбесіді **Q:** Що таке перевірка надмірних властивостей і коли вона спрацьовує? **A:** Спрацьовує при прямому присвоєнні літерала об'єкта типізованій змінній або при передачі як прямого аргументу. Передача через змінну пропускає перевірку, бо TypeScript розглядає об'єкт як вже існуючий. **Q:** Чи можуть два класи бути структурно несумісними навіть з однаковими методами? **A:** Так. Приватні або захищені члени роблять класи несумісними. Кожен клас є власником своїх приватних членів, і TypeScript розглядає їх як різні навіть якщо назви полів збігаються. **Q:** Як примусово використовувати номінальну типізацію в TypeScript? **A:** Брендуй тип унікальним символом: `type USD = number & { readonly __brand: unique symbol }`. Це додає фантомну властивість яка існує лише на рівні типів і блокує випадкові присвоєння. **Q:** Чому присвоєння свіжого літерала до супертипу дає помилку? **A:** TypeScript застосовує перевірку надмірних властивостей до свіжих літералів. Якщо цільовий тип не оголошує зайву властивість, TypeScript дає помилку. Присвой спочатку до змінної і перевірка пропускається. **Q:** Чи є витрати часу виконання від структурної типізації? **A:** Жодних. Вся типова інформація видаляється до запуску JavaScript. Структурна перевірка відбувається виключно під час компіляції. **Q:** (Senior) Тип функції `(e: string) => void`. Чи можна присвоїти `(e: string, n: number) => void`? **A:** Ні. Параметри функцій перевіряються контраваріантно. Зайвий параметр `n` робить сигнатури несумісними, бо той хто викликає функцію не надає його. ## Приклади ### Базовий: збіг форми об'єкта ```typescript interface User { id: number; name: string; } // зайва властивість 'role' - без implements, без класу const admin = { id: 1, name: "Alice", role: "admin" }; function greet(user: User): string { return `Привіт, ${user.name}`; } console.log(greet(admin)); // "Привіт, Alice" ✅ ``` У `admin` є зайва властивість `role`, але `User` вимагає лише `id` і `name`. TypeScript бачить обидві, тому виклик допустимий. Властивість `role` просто поза зором типу. ### Середній рівень: props React-компонента ```typescript interface ButtonProps { children: React.ReactNode; onClick?: () => void; className?: string; } const Button = ({ children, onClick, className }: ButtonProps) => ( <button onClick={onClick} className={className}> {children} </button> ); const handleClick = () => console.log("clicked"); const usage = ( <Button onClick={handleClick} className="btn-primary"> Submit </Button> ); // ✅ форма збігається з ButtonProps ``` `ButtonProps` не цікавить звідки береться `handleClick`. Перевіряється лише форма сигнатури функції. Структурна типізація в реальному продакшн-коді. ### Просунутий рівень: брендовані типи для номінальної поведінки Коли дві форми однакові але повинні залишатись окремими, брендовані типи (branded types) додають маркер на рівні типів: ```typescript // прості аліаси - структурно ідентичні, TypeScript не може їх розрізнити type USD = number; type EUR = number; // брендовані аліаси - структурно різні на рівні типів type BrandedUSD = number & { readonly __brand: unique symbol }; type BrandedEUR = number & { readonly __brand: unique symbol }; // функції-конструктори як єдиний спосіб створити значення function makeUSD(n: number): BrandedUSD { return n as BrandedUSD; } function makeEUR(n: number): BrandedEUR { return n as BrandedEUR; } function convertToEUR(amount: BrandedUSD): BrandedEUR { return (amount * 0.85) as BrandedEUR; } const dollars = makeUSD(100); convertToEUR(dollars); // ✅ convertToEUR(makeEUR(100)); // ❌ BrandedEUR не можна присвоїти BrandedUSD ``` Властивість `__brand` ніколи не існує під час виконання. Це фантомне поле, яке TypeScript використовує лише під час перевірки типів. Це стандартний спосіб отримати номінальну поведінку від структурної системи типів.Для рев’юераПримітка для модератора (необов’язково)Бачить лише модератор. Прискорює рев’ю.