Іменовані та типізовані параметри з значеннями за замовчуванням у TypeScript

текст перекладу
pic

Декілька місяців тому я почав працювати над новим проектом на основі Electron + Vue + TypeScript. Вже тоді я передбачав, що він з часом збільшиться в складності.

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

У своєму недавньому проекті на Python, який я розробляв на основній роботі, я використовував поєднання анотацій типів, іменованих аргументів та значень за замовчуванням (де це необхідно). Я виявив, що виклики функцій з іменованими аргументами робили код більш зрозумілим і легким для читання, коли я пізніше до нього повертався.

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

Швидкий приклад на Python

Нижче наведено приклад геометричного класу Line. Конструктор цього класу приймає два об'єкти Point. Це простий приклад, який демонструє, що конструктор чекає два аргументи типу Point, і що, якщо один з них відсутній, він встановлює значення за замовчуванням.

class Line:  
 def __init__(self,   
 p1: Point=Point(x: 0, y: 0),   
 p2: Point=Point(x: 0, y: 0)):  
 self.p1 = p1  
 self.p2 = p2  

# Створення лінії без аргументів  
l1 = Line()  

# Створення об'єкта Line, передаючи лише другий пункт  
l2 = Line(p2=Point(x: 0, y: 100))  

# Створення об'єкта Line, визначаючи обидва пункти  
l3 = Line(p1=Point(x: 0, y: 0), p2=Point(x: 100, y: 100))

Порівняйте це з наступним кодом, який не використовує анотації типів, іменовані аргументи чи значення за замовчуванням.

class Line:  
 def __init__(self, p1, p2):  
 self.p1 = p1  
 self.p2 = p2  

# Це не працює, бо обидва аргументи обов'язкові  
l1 = Line()  

# Теж не працює з тієї ж причини, що і l1  
l2 = Line(Point(0, 100))  

# Створює нову лінію  
l3 = Line(Point(0, 0), Point(100, 100))

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

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

Варіант на TypeScript

class Line {  
 p1: Point  
 p2: Point  

 constructor({  
 p1 = {x: 0, y: 0},  
 p2 = {x: 0, y: 0}  
 }: {  
 p1?: Point,  
 p2?: Point  
 } = {}) {  
 this.p1 = p1  
 this.p2 = p2  
 }  
}  

// Різні способи створення лінії  
const l1 = new Line() // Використовує значення за замовчуванням  
const l2 = new Line({p1: {x: 0, y: 100}})  
const l3 = new Line({  
 p1: new Point({x: 0, y: 0}),   
 p2: new Point({x: 0, y: 100})  
 })

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

Є кілька складних моментів, на яких варто зупинитися. По-перше, ми використовуємо деструктурування об'єктів для надання значень за замовчуванням. Щоб краще зрозуміти, як це працює, ось спрощений приклад:

const {x=2, y=4, z=8} = {x: 16, y: 32}  
console.log(x, y, z) // Виводить: 16, 32, 8

По-друге, зверніть увагу, що ми повинні використовувати ? перед двокрапкою, щоб вказати, які параметри є необов'язковими.
текст перекладу
Без цього компілятор TypeScript виведе помилку через відсутні параметри.

Інша особливість, яку ви могли помітити, це те, що p1 і p2 визначені як прості об'єкти, кожен з яких має властивості x і y. Це допустимо, оскільки підпис об'єкта відповідає інтерфейсу класу Point. Це означає, що {x: 0, y: 0} є допустимою заміною для екземпляра класу Point.

Нарешті, ми повинні відзначити, що весь вираз у конструкторі дорівнює {}. Як це незвично? Ця особливість дозволяє нам передавати нуль параметрів у конструктор, як у випадку з створенням l1. Варто зазначити, що ми включаємо це тільки в тому випадку, якщо функція чи конструктор можуть бути виконані без параметрів (тобто всі параметри мають значення за замовчуванням).

Останні думки

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

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

Як я можу написати конструктор або функцію TypeScript, яка приймає типізовані, іменовані аргументи з значеннями за замовчуванням?

interface Options {  
 a?: number; // Необов'язковий параметр зі значенням за замовчуванням  
 b?: string; // Необов'язковий параметр зі значенням за замовчуванням  
 c?: boolean; // Необов'язковий параметр зі значенням за замовчуванням  
}  

class Example {  
 a: number;  
 b: string;  
 c: boolean;  

 constructor({ a = 42, b = "default", c = true }: Options = {}) {  
 this.a = a;  
 this.b = b;  
 this.c = c;  
 }  
}

Як видно, це дуже схоже рішення, з додаванням інтерфейсу, що визначає список параметрів.

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

Перекладено з: Named & Typed Parameters with Default Values in TypeScript

Leave a Reply

Your email address will not be published. Required fields are marked *