Frontend [TypeScript] 1
Type vs Interface
Як інструменти типізації у багатьох випадках різниця буде непомітною.
Все окрім “implements"
- можна реалізувати через “type"
;
type User = { name: string; age: number; };
interface IUser { name: string; age: number; }
Багато людей використовує наступні правила:
Для об’єктів - інтерфейси
Для всього іншого - типи
interface IUser {
name: string;
age: number;
status: "registered" | "blocked";
}
Такий код добре читається, але якщо замінити “interface"
на “type"
то великої різниці не буде, професіонали називають це - “вкусовщина".
type Status = "registered" | "blocked";
interface IUser {
name: string;
age: number;
status: Status;
}
Опираючись на правила які описані вище саме так потрібно використовувати взаємодію “type"
та “interface"
Type Merging
interface IUser {
name: string;
}
interface IUser = {
age: number;
}
const user: IUser = {
name: 'Jhon',
age: 27,
} // OK
тепер типи
type User = { name: string };
type User = { age: number };
// уже на цій стадії буде помилка (Duplicate identifier 'User'.)
Механізм мержингу - це те що дає більшу гнучкість при написанні типів, але також може заплутувати розробників які погано знайомі з цією темою, будьте уважні і обережні при використанні цієї техніки.
Tuples
interface ITuple extends Array<string | number> {
0: string;
1: string;
2: number;
}
Виглядає кріпово і незрозуміло - не робіть так.
Варіант для нормальних людей.
type Tuple = [firstName: string; lastName: string; id: number];
При порівнянні 2й варіант виглядає значно краще і читабильніше, звісно можна написати просто:
type Tuple = [string; string; number];
Але такий варіант значно менше інформативний, використовуйте попередній варіант - команда буде вам вдячна.
Я не буду розповідати всі комбінації можливих реалізацій через type
чи interface
- це тема окремої статті. Тут лише хочу донести головні ідеї які вплинуть на читабельність та швидкість писання коду.
keyof | typeof | in
keyof
Порівнюючи з JavaScript у якому є метод об’єкту Object.keys()
у TypeScript маємо keyof
який повертає набір [“union"]
не індексованих значень об’єкту або масиву (у JS теж є об’єктом).
type EntityType = {
name: string;
age: number;
id: number;
}
type EntityTypeKeys = keyof EntityType; // 'name' | 'age' | 'id';
Тепер можна використовувати EntityTypeKeys
як окремий набір значень.
type CapitalizedType = {
[k in EntityTypeKeys as `${Capitalize<k>}`]: EntityType[k];
}
// { Name: string; Age: number; Id: number; }
тут уже знадобились шаблонні рядки які згадувались у попередній статті.
typeof
якщо вам потрібно перетворити уже створений об’єкт у тип, можна використовувати typeof
const user = { name: 'Json', age: 27 };
type UserType = typeof user;
// { name: string; age: number }
інколи такий підхід може бути дуже корисним, але звісно є деякі незручності.
Якщо поле об’єкту може бути двох типів то через оператор typeof
ми отримаємо не коректний тип.
const user = {
name: 'Jhon',
age: 27,
status: 'registered' // can be number (1, 2, 3)
}
type UserType = typeof user;
// { name: string; age: number, status: string }
type CorrectUserType = Omit<UserType, 'status'> & {
status: string | number;
};
// { name: string; age: number, status: string | number }
in
Для перевірки чи дійсно є поле у об’єкту, можна використати декілька варіантів.
type Employee = {
name?: string;
department?: string;
country?: string;
};
const emp: Employee = {};
// 👇️ (property) department?: string | undefined
emp.department
if (emp.department !== undefined) {
console.log(emp.department.toLowerCase());
}
if (emp.hasOwnProperty('department')) {
console.log(emp.department.toLowerCase());
}
// ---
if('department' in emp) {
console.log(emp.department.toLowerCase());
}
Перший варіант не коректний, він не перевіряє чи є таке поле у об’єкті, насправді він перевіряє чи значення цього поля дорівнює undefined
, що не правильно.
Другий варіант правильніший для перевірки існування поля у об’єкті, навіть якщо значення undefined
.
Третій варіант краще використовувати для швидкої перевірки існування певного поля у об’єкті, але він не дуже надійний.
const entity = { name: 'Jhon' };
'constructor' in entity // true
'__proto__' in entity // true
'hasOwnProperty' in entity // true
// У той час як hasOwnProperty
entity.hasOwnProperty('constructor'); // false
entity.hasOwnProperty('__proto__'); // false
entity.hasOwnProperty('hasOwnProperty'); // false
Потрібно пам’ятати при роботі з подібними назвами полів, для себе я порівнюю роботу“hasOwnProperty”
та“in"
як різницю між“for of"
та“for in"
відповідно.
“in” варто використовувати якщо ви впевнена у собі людина.
Оператори
& (Intersection)
& - поєднання типів. Щось типу впевненої у собі кон’юнкції.
type A = B & C;
B, C
- можуть бути як типами так і інтерфейсами, але однакової структури.
Якщо структура відрізняється - отримаєте помилку.
extends
використовується тільки з інтерфейсами, ця логіка закладена для трушних ООП розробників, та просто меломанів.
interface A extends B {
field: string
}
// --
interface A extends B, C {};
Також можна наслідуватись від об’єктних типів, але не від Mapped Types
Type A = { field: string };
Type B = { count: number };
interface C extends A, B {}
const entity: C = {} // { field: string; count: number; }
| (Union)
Оператор - union
, використовується для декларації можливих варіантів типів.
type A = B | C;
багато уже де це використовувалось, тому зупинятись на цьому немає сенсу.
-? та +?
Поля об’єкту можуть бути обов’язковими або опціональними, для таких полів можна використати наступні типи.
type Entity = { name: string };
type OptionalEntity = Partial<Entity>; // всі поля стали не обов'язковими
type RequiredEntity = Required<OptionalEntity>; // всі поля стали обов'язковими
Якщо говорити про реалізацію то ось як це можна зробити самостійно.
type CustomPartial<D> = {
[k in keyof D]+?: D[k];
}
// або просто
type CustomPartial<D> = {
[k in keyof D]?: D[k];
}
і навпаки
type CustomRequired<D> = {
[k in keyof D]-?: D[k];
}
Інколи потрібно створити вкладені типи, у таких випадках це буде корисним.
Підсумки
Всі ці механізми та оператори TypeScript описані у документації, але часто пропускаються. Справді не велика різниця між використання type
чи interface
і шанс того що мержинг типів зламає вам проєкт - мінімальний.
Для того щоб називати себе професіоналом певної галузі, вам варто розуміти всі її аспекти, навіть якщо це щось не явне або абстрактне.
У наступних статтях будемо розбирати складніші та набагато цікавіші речі, про які уже не так багато інформації у документації.
UPD: не було натхнення писати про “implements"
тож допишу як з’явиться 🙃