Duck-Typing у TypeScript та правило “Always Defaults”

💡 Усі статті, обговорення, новини про Front-end — в одному місці. Приєднуйтесь до Front-end спільноти!

Quacking and Swimming Duck

TypeScript, як надмножина JavaScript, додає статичну типізацію до динамічного світу розробки на JavaScript. Однією з його основних функцій є duck-typing (типізація «як качка»), що дозволяє створювати лаконічний і водночас безпечний код. У поєднанні з правилом Always Defaults («Завжди використовуйте значення за замовчуванням») розробники можуть мінімізувати помилки типізації та типові проблеми на етапі виконання, роблячи код більш стабільним і надійним.

У цій статті ми розглянемо duck-typing у TypeScript, переваги використання значень за замовчуванням замість явного оголошення типів для змінних і те, як це правило допомагає уникати таких помилок виконання, як «object has no property or method».

Що таке Duck-typing у TypeScript?

Duck-typing базується на принципі: «Якщо це виглядає, як качка, плаває, як качка, і крякає, як качка, то, ймовірно, це качка.» У TypeScript цей принцип дозволяє перевіряти типи об’єктів на основі їхньої структури, а не оголошених типів.

Наприклад:

let isTyped = true; // typeof isTyped is boolean by default 
let index = 0; // typeоf index is number by default 
let name = ''; // typeof name is string by default
const duck = {
  quack: () => console.log("Кря!"),
}
interface Quackable {
   quack(): void; 
}
const makeItQuack = (animal: Quackable) => {
  animal.quack();
}
makeItQuack(duck); // Працює, тому що `duck` має метод `quack`

У цьому прикладі об’єкт duck відповідає структурі інтерфейсу Quackable, тому TypeScript дозволяє його використання без явного оголошення типу.

Правило «Always Defaults»: чому значення за замовчуванням краще за типи

Оголошуючи змінні та параметри функцій додавання значень за замовчуванням може бути більш практичним захищенням від помилок рантайму, ніж тільки визначення типу. Це правило гарантує, що змінні завжди матимуть як тип, так і початкове значення, зменшуючи ризик виникнення помилок на етапі виконання, пов’язаних з undefined або null.

Приклад без значень за замовчуванням

Розглянемо наступну ситуацію:

function greet(name: string) {
  console.log(`Привіт, ${name}!`);
}
greet(); // Помилка: Очікується 1 аргумент, але отримано 0.

Якщо при виклику функції не вказати параметр name, TypeScript викличе помилку компіляції. Це може призвести до проблем у коді, якщо не бути уважним.

Приклад зі значеннями за замовчуванням

Тепер застосуємо правило Always Defaults:

function greet(name = "Гість") {
  console.log(`Привіт, ${name}!`);
}
greet(); // Виведе: Привіт, Гість!

У цьому випадку, якщо значення name не передано, воно автоматично встановлюється як «Гість». Такий підхід усуває помилки на етапі виконання та забезпечує передбачувану поведінку.

Переваги правила «Always Defaults»

1. Покращення читабельності функцій

  • Розробникам буде легше зрозуміти призначення функції, якщо вони бачитимуть значення аргументів
  • Розмір коду зменшиться завдяки видаленню зайвих typescript анотацій
  • Завдяки параметрам за замовчуванням функції залишаються зручними навіть у разі пропуску деяких аргументів

2. Усунення помилок undefined та null, «object has no property or method»

Значення за замовчуванням гарантують, що змінні завжди ініціалізовані валідним значенням, усуваючи у більшості випадків потребу в зайвих перевірках.

Duck-typing у поєднанні зі обов’язковими значеннями за замовчуванням забезпечує, що передані об’єкти містять усі необхідні властивості чи методи, запобігаючи помилкам під час виконання.

3. Сумісність з JavaScript

Якщо ви використовуєте правило Always Defaults на JavaScript проектах, це вже зробить ваш код максимально сумісним з TypeScript, що значно полегшить подальший перехід на повну статичну типізацію.

Недоліки правила «Always Defaults»

Використання правила «Always Defaults» має багато переваг, але є також кілька потенційних ситуацій, у яких воно може бути неефективним.

1. Маскування логічних помилок

Якщо значення за замовчуванням задається без належного аналізу, це може приховувати помилки у коді.

  • Коли важливі обов’язкові значення. Якщо деякі змінні чи аргументи повинні бути явно переданими (наприклад, userId), дефолтні значення створюють ілюзію «коректної» роботи програми.
  • Коли є складна бізнес-логіка. У випадках, коли значення має бути отримане з конкретного джерела, дефолтне значення може приховати помилки в цьому джерелі (наприклад, помилка в API).
  • Відсутність валідаторів. Без перевірки вхідних даних програма може виконуватися неправильно, не показуючи, що щось пішло не так.

2. Циклічна ініціалізація

Задаючи значення за замовчуванням циклічно, можна створити великі масиви, які насправді не потрібні, що призведе до марного витрачання пам’яті або ресурсів.

3. Не оптимально для залежностей

Якщо значення за замовчуванням залежить від зовнішніх ресурсів (наприклад, результату виклику API або іншої функції), використання «Always Defaults» може ускладнити або сповільнити виконання коду:

const getDefaultValue = () => fetch("https://api.example.com/config");
let config = getDefaultValue(); // Виникає затримка через асинхронність.

Як уникнути: Використовувати lazy initialization (відкладену ініціалізацію) для складних або асинхронних значень.

Практичне застосування

Значення за замовчуванням для змінних

Замість цього:

let count: number;
count++; // Помилка: Змінна 'count' використовується до присвоєння значення.

Краще так:

let count = 0;
count++; // Працює без проблем

Значення за замовчуванням для параметрів функцій

Замість цього:

function fetchData(url: string, retries?: number) {
  const attempts = retries || 3;
  console.log(`Завантажую ${url} з ${attempts} спробами`);
}

Краще так:

function fetchData(url = "", retries = 3) {
  console.log(`Завантажую ${url} з ${retries} спробами`);
}

Значення за замовчуванням у деструктуризації об’єктів

Коли працюєте з об’єктами, задавайте значення за замовчуванням:

function configure({ retries, timeout } = { retries: 3, timeout: 1000 }) {
    console.log(`Кількість спроб: ${retries}, Таймаут: ${timeout}`);
}
configure(); // Виведе: Кількість спроб: 3, Таймаут: 1000

Висновки

Duck-typing дозволяє TypeScript перевіряти типи на основі структури даних, а не оголошених типів.

Правило «Always Defaults»

Завжди використовувати значення за замовчуванням — не найкраща ідея. Можливо, це допоможе уникнути падіння застосунку під час виконання, але водночас створює ідеальні умови для появи багів і логічних помилок. У результаті додаток працює без видимих збоїв, проте не виконує очікувані дії.

Обов’язкові аргументи функцій критично важливі для коректної роботи. Уявімо функцію, яка надсилає електронні листи. Для неї обов’язковими параметрами є адреса отримувача і вміст листа. Як розробника, тестувальника або кінцевого користувача мене абсолютно не влаштує ситуація, коли помилка не виникає, але лист не доходить до потрібного отримувача. Наявність явної помилки дозволила б одразу зрозуміти, що щось пішло не так.

👍ПодобаєтьсяСподобалось4
До обраногоВ обраному0
LinkedIn
Дозволені теги: blockquote, a, pre, code, ul, ol, li, b, i, del.
Ctrl + Enter
Дозволені теги: blockquote, a, pre, code, ul, ol, li, b, i, del.
Ctrl + Enter

Категорично не погоджуюсь з цим підходом, можливо ви вирішуєте проблему падіння у рантаймі, але разом з тим створюється неймовірний простір для багів і якихось логічних помилок, коли апка наче працює, не викидає помилки але не виконує те, що від неї очікується. Ну і щодо скажімо аргументи функції, обов’язкові аргументи вони на те і обов’язкові, що вони необхідні для коректного виконання функції, припустимо у мене є функція, яка має відправляти емейл з певним контентом, її обов’язкові аргументи це емейл отримувача і його контент, і мене (як і тестувальника чи кінцевого користувача) взагалі не влаштує що помилки не було, але емейл не надіслався куди потрібно, якби була помилка, було б хоча б одразу зрозуміло, що щось пішло не так. Тому підхід «Fall Fast» є набагато більш дієвішим, ніж описаний у статті, цей підхід полягає у тому, що якщо у нас є якась некоректний стейт чи поведінка, ми викидаємо помилку, або взагалі крашимо апку, таким чином ми швидко можемо зрозуміти, що є проблема і її треба вирішувати, і по стек трейсу помилки, за умови коректно налаштованого логування, можна ще й одразу зрозуміти де вона виникла, а з підходом запропонованим у статті можна годинами ходити колами і розбиратися що пішло не так

На мою думку, за замовчуванням варто встановлювати лише ті параметри, які не впливають на очевидність роботи коду. Тобто, якщо це якась конфігурація, і я передаю туди лише одне поле для зміни поведінки, всі інші поля мають працювати за замовчуванням — і це очевидно.
А ось значення на кшталт username = «Guest» не є однозначно очікуваним, адже перше, що спадає на думку, — це те, що таке значення надійшло з бекенду.

function greet(name = "Гість") {
  console.log(`Привіт, ${name}!`);
}
greet(); // Виведе: Привіт, Гість!

Це для мене просто антіпаттерн. Це фактично дозволяє розробнику викликати функцію без обовʼязкового параметру та вбиває весь тайпчекінг в цьому кейсі.

так default value явно каже «параметр не обов’язковий».
у статті в недоліках якраз вказано

Якщо деякі змінні чи аргументи повинні бути явно переданими (наприклад, userId), дефолтні значення створюють ілюзію «коректної» роботи програми.

Тобто у функціях always defaults в принципі тільки для опціональних параметрів і підходить.

Ну просто приклад тоді недоречний. Хто буде хардкодити name = «Гість» в подібній функції?

А я думаю приклад «хелло гість» цілком схожий на правду — якщо треба відобразити вітаннячко на публічній сторінці, то елементу геть по барабану чи там гість чи юзер. Інша справа якщо це вже особистий кабінет, де має бути юзер — тут використовувати «guest» не просто недоречно, а ще й шкідливо.

А я вважаю, що й до особистого кабінету можна пускати гостя в ридонлі моде. Але не завжди. Залежить від конкретного сервісу.

ну це дуже сумнівно. В ro пускати юзера можна — якась демка чи щось таке — але тоді ліпше зробити якогось DumbUser { name: "Guest" } і юзати його в межах кабінету, аніж в кожнісінькому методі кабінету реалізовувати свій власний ro режим.

Доречно, що тут перелічені недоліки підходу. Саме через деякі з них я обираю використовувати дефолтні значення як опцію, а не як правило. Там де зручно — будь ласка! Часом для потреб мого проекту зручно перевести значення інпутів у порожню строку, якщо відсутні, тоді так. Але коли аргумент, це не примітивний тип, а об’єкт, скажімо, options, config тощо, то тут вже складніше.
А ще дефолтні значення треба звідкись взяти. От retries = 3. А чому стільки? Це ж хтось має вирахувати, що така кількість оптимальна.

так, краще зробити константою:
retries = OPTIMAL_RETRIES_COUNT

Мав задоволення працювати з кодбазою де використовувався принцип «Always Defaults». З досвіду можу сказати, що дефолтні значення зазвичай створюють тільки більше багів, які при цьому ще і складніше відловити, бо спробуй знайти де по дефолту виставилося значення «Guest». Саме тому краще мати nullable тип, який Typescript знайде і підсвітить, ніж string з дефолтним значенням

Оце так знахідка! Чи можна дізнатись подробиці? Стек, об’єм codebase?

Чи застосовували інструменти для аналізу коду в цьому проєкті? Він ще живий?

В цьому і проблема, що інструменти для аналізу коду не показують, що десь використовуються дефолтні значення. Кодова база Typescript і C++. Розробники переводили застосунок з Qt і з цієї бази потягнули звичку використовувати дефолтні значення

Шах и мат:
www.typescriptlang.org/...​OhgCQC2 GvoJbvo8ISE0oRYQA
DuckTyping сознательный косяк для совместимости с JS

Потому что в твоем примере — не утиная типизация.
Косяк — потому что потенциальных источник сложноотлавливаемых проблем в проектах чуть сложнее Hello World.

В якому з моїх прикладів не качкова типізація?
Наведіть будь-ласка приклад «потенциальных сложноотлавливаемых проблем»

В обоих. На самом деле можно похоливарить на тему, что именно называть утиной тпизацией, но факт остается: в моем примере утиная типизиация не работает. В твоих для того, чтобы она заработала (или назовем это неявным выделением интерфейса) потребовались дополнительные приседания.
Вопрос: почему Хейлсберг не добавил поддержку кода из моего примера, хотя там утиней некуда?

ваш код — чистий ООП. Там немає міста всеяким припущенням.
Тож качкова типізація в строгих ООП-мовах можлива, але її реалізація потребує або порушення їх принципів, або використання специфічних механізмів, таких як рефлексія чи динамічні проксі.

Ваш приклад не працює, тому, що (new Vehicle) іinstanceof Task == false

ваш код — чистий ООП. Там немає міста всеяким припущенням.

засим откланиваюсь.

Наведіть будь-ласка приклад «потенциальных сложноотлавливаемых проблем»

Переименование вызываемого метода или изменение параметров в любом из классов.
В лучшем случае — ошибка компиляции. Придется разбираться со всеми функциями, в которых «утино» вызывается метод. А их может быть десятки и ты вообще не понимаешь кто когда и зачем их написал. Заодно надо будет разбиратсья со всеми классами, которые имеют одноименный метод, который «утино» вызывается везде. Если проект сложней HelloWorld, то это проблема.
В худшем — зафейлится в рантайме при каких то специфических условиях, типа
if(a>b&&d<c) return new Task();
В случае со статической типизацией это делается на раз-2 и во многих случаях — одним тыком мыши в меню «refactor».

Ваше розуміння є повністю обʼєктно-орієнтованим. Моя стаття орієнтована на react-based проєкти та функціональну парадигму програмування.

Я тебе оеисал проблему, типичную для утиной типизации. Вне зависимости от стиля написанного кода. А ты закрываешь на нее глаза.

Так, ця проблема може існувати і в функціональних мовах програмування, хоча її прояви будь дуже слабкими, якщо викориристовується:

* Функціональна композиція. Уникнення глибокої ієрархії класів і методів. Замість цього, у функціональних мовах перевага надається композиції функцій, що зменшує кількість точок, які можуть спричинити конфлікти.

* Функціональні патерни. Наприклад, використання Higher Order Functions (HOF), параметричних типів чи Monad для контролю над логікою дозволяє зменшити кількість помилок, пов’язаних із повторенням або рефакторингом.

* Використання алгебраїчних типів даних і строгих сигнатур функцій робить код більш передбачуваним.

Эт ты серьезно? :))
Не надо мне читать мантры, я их знаю лучше тебя. Ты расскажи как конкретно ты будешь решать проблему изменения сигнатуры функции/метода/колбека на большом проекте(миллионы LOC) с активным использованием утиной типизации.

я додам нову функцию/метод/колбек, замість редагування існуючого. За SOLID принципом Open-Closed.

Я рад за тебя, но как этот метод начнет взаимодейстовать со старым кодом?

а стару помічу як deprecated. Новий функціонал оформлю як wrapper або adapter.

Это все прекрасно, но ты не ответил на впрос: как измененная сигнатура функции будет вызываться во всех тех же местах что и ранее?

во всех тех же местах что и ранее буде викликатись deprecated функція. А новий функціонал буде викликати нову функцію.

На проекте в миллионы LOC и это ничего не поломает. Останется только мелочь: вызывать во всех нужных местах нужную версию функции. Ок. Удачи.

ні «нужную», а нову версію. В новому коді неможна використовувати deprecated функціонал. Від цього старого коду девелопери будудь збавлятись поступово, тільки тоді, коли протестовані усі зміни функціоналу, на який може вплинути зміна коду.

этотраьотает разве что в мире розовых пони. Новый вункциогал должен работать не поломав старый. Прямо сегодня.. Давай конкретно, опиши свои шагр что ты будешь делать чтобы решить описанную проблему

Your comments feel a bit off to me, and I’d prefer not to continue the conversation this way. It might be worth exploring ways to refine your communication skills in the future.

You may continue to ignore my comments by commenting on them.

ось варіант з повною зворотною сумісністю:
www.typescriptlang.org/...​HhD9QoPDYyaoWmJTswtLK1hAA

Я ж не утверждаю, что нельзя обойти. Но то, что для обхода нужен 3й тип, немного намекает что скоуп утиной типизации сознательно уменьшили до минимально необходимого.

Давайте підійдемо до нашої дискусії таким чином:
Я стверджую, що значну кількість широковикористуюмих TypeScript декларацій можливо переписати використовуючи качкову типізацію.
Ви не сгодні?

Тоді я не розумію чого вам не подобається качкова типізація з правилом AlwaysDefaults, якщо це є майже повнофункційною альтернативою анотаціям TypeScript. Типізувати любу JS декларацію можна як TypeScript анотацією, так і ініциалізацією значенням за замовчуванням.

Тоді я не розумію чого вам не подобається качкова типізація

Я прямым тексто написал выше: семантика основанная на имени функции(уцтиная типизация) не дает возможности делать рефакторинг в сложных проектах.

Ця стаття не о том, що утіну типізацію треба використовувати всюди та вєздє замість інтерфейсів та іньших абстракцій. Мета статті у тому, що явні тайпскріпт анотації джаваскріптових декларацій можна замінити неявним типизуванням шляхом присвоювання значень за замовченням. Це ні як не впливатиме на сложність подальшого рефакторінгу.

Мета статті у тому, що явні тайпскріпт анотації джаваскріптових декларацій можна замінити неявним типизуванням шляхом присвоювання значень за замовченням.

Извини, я грубо напишу, но уже надоело обьяснять банальщину:
«дай дураку стеклянный буй, так он и буй разобьет и руки порежет»

I find your conversation style a bit unsuitable, and I’d prefer not to continue the discussion with you. I’d appreciate it if we could stop here. Perhaps focusing on refining your communication approach could help in the future. Thank you for understanding!

Well, happy for you.
I expect that it is not a kindergarten here and showing up a typical issue will not lead to copypasting of a marketing bullshit and then stating that “I can rewrite everything” — you may rewrite, but who will support it on a scale and what is the price?
Get experience on a bigger projects and come back with the lessons learned.

ось основна ідея статті:

Правило «Always Defaults»: чому значення за замовчуванням краще за типи

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

Правило «Always Defaults»

Скажи, и лучше дай ссылку, где я писал что это плохо?
Но у тебя вторая идея статьи — утиная типизация, которую ты связал с Always Default — и опять таки, я ни разу не критиковал твое твое решение свзяать эти 2 кейса, хотя мне это выглядит несколько странно.
Мы с тобой общаемся про утиную типизацию, и я тебе указываю, что на масштабе это проблема и геморр, указывая конкретные кейсы. Ты мне в ответ пишешь что

* Використання алгебраїчних типів даних і строгих сигнатур функцій робить код більш передбачуваним.

Да неужели :)

I noticed you seem to be a duck-typing hater. If that’s the case, I’d love to hear how you manage to avoid it in your very big projects? 😊 (It’s a built-in feature of TS, duck-typing cannot be just turned off with some flag or directive)

Just curious: what is the root cause of your switching to English?

честно говоря надуманная проблема какая-то.
дефолтные значения это хорошо, но нужно правильно их использовать.

Розглянемо наступну ситуацію:

function greet(name: string) {
console.log(`Привіт, ${name}!`);
}
greet(); // Помилка: Очікується 1 аргумент, але отримано 0.
Якщо при виклику функції не вказати параметр name, TypeScript викличе помилку компіляції. Це може призвести до проблем у коді, якщо не бути уважним.

Приклад зі значеннями за замовчуванням
Тепер застосуємо правило Always Defaults:

function greet(name: string = «Гість») {
console.log(`Привіт, ${name}!`);
}
greet(); // Виведе: Привіт, Гість!
У цьому випадку, якщо значення name не передано, воно автоматично встановлюється як «Гість». Такий підхід усуває помилки на етапі виконання та забезпечує передбачувану поведінку.

если бы код собрался — то
ошибки на этапе выполнения и так бы не было, было бы «Привiт, undefined»

greet(); // Помилка: Очікується 1 аргумент, але отримано 0.
Якщо при виклику функції не вказати параметр name, TypeScript викличе помилку компіляції. Це може призвести до проблем у коді, якщо не бути уважним.

к каким проблемам в коде это может привести, если тайпскрипт скажет что ошибка.

let count: number;
count++; // Помилка: Змінна ’count’ використовується до присвоєння значення.

этот код вообще странный

дякую за коментар. Так, приклади не дуже гарні. Подумаю над покращенням.

надуманная проблема

Помилки з undefined та null — одні з найчастіших. Чи не є практика завжди додавати дефолтні значення змінним рішенням цих проблем? Може така практика привести до виникнення помилок іншого роду?

Так тут немає помилки, просто вывод будет некрасивый.

Я считаю что присваивать дефолтное имя лучше в другом месте, там где определяется имя пользователя. А функция greet должна просто делать приветствие.

Підписатись на коментарі