текст перекладу
Декілька місяців тому я почав працювати над новим проектом на основі 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