TypeScript - JavaScript тільки з типами, якось так коротко можна це описати щоб всі зрозуміли.
TypeScript - може як прискорювати роботу так і уповільнювати її в залежності від правильності використання, приклади:
При типізації API - це явно прискорюватиме роботу як для людей які уже працюють над проєктом, так і для людей які будуть над ним працювати. [USEFULL]
Типізація все-можливих подій по типу клік на елемент який знаходиться всередині іншого елементу лише для того щоб взяти властивість висоти елементу - крінж. [USELESS]
У цій статті будуть певні приклади коду та техніки використання можливостей TypeScript які спростять та збільшать швидкість написання коду.
Примітиви
Загальні типи
Документація говорить що є всього 3 примітивних типи:
type Primitive = string | boolean | number;
Можна також додати сюди symbol - 99% випадків вам це не знадобиться.
Також можна розширити і додати undefined, null, але це не правильно, так як у undefined, null - немає ніяких методів. Краще буде зробити окремий тип Nullable і використовувати оператор “?” - для випадків з undefined;
type Nullable<T> = T | null;
type Primitive = string | boolean | number | symbol | null | undefined;
type DTO = { field: Primitive };
type Primitive1 = string | boolean | number | symbol;
type DTO1 = { field?: Nullable<Primitive> };
При роботі із першим типом ви завжди будете отримувати помилку - “'.field' is possibly 'null' or 'undefined'.”
При роботі з другим варіантом ви будете отримувати ці повідомлення тільки якщо обернете у Nullable або поле буду опціональним, це дасть вам більшу гнучкість при типізації.
Оверхед
При роботі з примітивами супер-типізація може згубно впливати на швидкість написання коду.
Потрібно враховувати межу коли це - “Упоротий" або “Валідний" код.
приклад:
const [count, setCount] = useState<number>(0); // OK
const [count, setCount] = useState<0 | 1 | 2>(0); // not OK
У другому випадку може бути валідно тільки у якщо не потрібно щоб count був щось окрім “0, 1, 2", але краще це опрацювати на рівні своєї сет-функції
const handleUpdateCounter = (newCount: number) => {
if(newCount > 2) return;
setCount(newCount);
}
Місце де можна використати щось типу такого - це статуси або конкретні числові або текстові значення.
type USER_STATUS_TYPE = "REGISTERED" | "VERIFIED" | "BLOCKED";
але також потрібно враховувати те що вам потрібно буде використовувати значення цього типу для перетворення або порівняння, тоді краще використати enum.
enum USER_STATUS_ENUM {
REGISTERED = "REGISTERED",
VERIFIED = "VERIFIED",
BLOCKED = "BLOCKED"
}
const userStatus: USER_STATUS_ENUM = ...;
для складних операцій із подібними типами, або для використання такого роду типів за межами рендеру - краще обрати об’єкти.
const USER_STATUS_ENUM = {
REGISTERED: "REGISTERED",
VERIFIED: "VERIFIED",
BLOCKED: "BLOCKED"
}
type UserStatus = keyof typeof USER_STATUS_ENUM;
const isValidType = (userStatus: UserStatus) => Object.values(USER_STATUS_ENUM).includes(userStatus);
Звісно при грамотній типізації цей кейс вам не доведеться використовувати, але якщо текстові данні потрапляють із форми яку ввів користувач, або ви дуже не довіряєте API то додати подібну перевірку - зайвим не буде.
Шаблонні рядки
Це один із видів заплутування розробників, який у більшості випадків використовується людьми які знають що їм потрібно.
type StringUrlPattern = `http://${string}.${string}`;
const url: StringUrlPattern = 'http://google.com' // - correct;
const url1: StringUrlPattern = 'http://google' // - error;
Крута штука, яку варто знати, але також це може викликати багато проблем:
Уповільнює рефакторинг
Без коментарів новим розробникам ваші рішення можуть бути не очевидними.
При зав’язуванні всієї логіки певного модуля на таких типах, його розширення - це дуже складна задача, бо новий функціонал не завжди ідеально натягується на старий.
type StringUrlPattern = `http://${string}.${string}`;
const url: StringUrlPattern = 'http://google.com' // - correct;
const url1: StringUrlPattern = 'http://google' // - error;
const url2: StringUrlPattern = 'http://google.com.com.com' // - correct;
const url3: StringUrlPattern = 'http://..' // - correct;
Ось які проблеми можуть чекати на вас якщо довіряти лише типам, це не очевидні речі, які можуть викликати додаткову купу функцій-валідацій, які у свою чергу перекриють всю ідею шаблонних рядків.
Враховуйте ці пункти при використанні шаблонних рядків.
Допоміжні типи
type Str1 = Uppercase<"text">; // TEXT
type Str2 = Lowercase<"TEXT">; // text
type Str3 = Capitalize<"text">; // Text
type Str4 = Uncapitalize<"Text"> // text;
type Str5<S extends string = 'TEST'> = `${Lowercase<S>}-%{S}; // test-TEST
По назвах цих типів уже можна зрозуміти як вони працюють, якщо їх використовувати із шаблонними рядками - це створить щось магічне і неочевидне, тому варто використовувати у випадках коли ви дійсно впевнені у користі цих типів.
never | unknown | any
any
any - будь-яке значення. Але правильніше говорити що any - просто ігнорує типи, а не є чим завгодно. Можливі різні комбінації:
type Type = any;
type Type1 = any[];
type Type1 = Record<string, any>;
const entity = {} as any;
never
never - ніколи. Це означає що цього значення ніколи не може бути, найчастіше використовується у функціях щоб сказати що ця функція ніколи нічого не повертає, якщо ви напишете return у такій функції - отримаєте помилку.
Лайфхак:
const USER_STATUS_ENUM = {
REGISTERED: "REGISTERED",
VERIFIED: "VERIFIED",
BLOCKED: "BLOCKED",
}
type Status = keyof typeof USER_STATUS_ENUM;
function assertNever(value: never): never {
throw new Error(`Unexpected value: ${value}`);
}
function getStatusLabel(status: Status) {
switch (status) {
case "BLOCKED":
return 'Block';
case "VERIFIED":
return 'Verify';
case "REGISTERED":
return 'Register';
default:
assertNever(status); // If you forgot update 'cases', TypeScript will show compile-time error here.
}
}
Якщо додати ще одне поле у об’єкт USER_STATUS_ENUM - ви отримаєте помилку “Argument of type 'string' is not assignable to parameter of type 'never'.” - за допомогою таких маленьких лайфхаків можна зекономити собі багато часу при розширенні коду.
unknown
unknown - менше зло ніж any але теж маленьке зло. Цей тип використовується коли ви хочете позначити щось, але не знаєте що там, у той час як any - відключає типізацію, unknown - говорить що тут буде якась значення, але поки не ясно яке, при роботі із unknown його потрібно переприсвоювати.
type Response = { name: string };
const a = apiResponse as unknown as Response;
так краще не робити, але це приклад того як можна використати unknown.
const data: unknown = getValueFromExternalLibraryWithoutTypes();
у таких випадках unknown - це найкращий тип який можна використати.
const data: unknown = getValueFromExternalLibraryWithoutTypes();
let localData: string;
localData = data; // Error;
якби тут ми використали any - була б біда.
Підсумки
Лише з примітивами можна значно покращити “DX” команди при роботі з TypeScript.
Деякі із цих речей очевидні і можуть здаватись занадто простими, але про них потрібно пам’ятати щоб використовувати всі можливості TypeScript.
На цьому завершу. Розкрити всі теми і лайфхаки TypeScript у одному довгочиті дуже складно, і це буде не інформативно, тому у наступних статтях буде більше цікавих можливостей TypeScript.