TypeScript і все, що тобі потрібно в розробці
Ця стаття призначена для полегшення процесу вивчення TypeScript за допомогою практичних прикладів. Докладнішу інформацію можна знайти в документації або додаткових матеріалах.
Стаття призначена як для розробників-початківців, які тільки починають знайомитися з TypeScript, так і для досвідчених розробників, які бажають поглибити свої знання в цій мові. Тут ви знайдете короткий та інформативний виклад ключових аспектів TypeScript, які можуть бути корисними у повсякденній розробці. Для вашої зручності, зміст статті містить посилання на конкретні теми TypeScript, так що ви можете швидко перейти до частини матеріалу, що вас цікавить.
Intersection Types и Union Types
Intersection Types
У TS можна перетинати типи. Ви можете отримати тип C способом перетину типів А та В. Дивіться приклад нижче:
type A = {
id: number
firstName: string
lastName: string
}
type B = {
id: number
height: number
weight: number
}
type C = A & B
//Підсумок перетину типів A та B
type C = {
id: number
firstName: string
lastName: string
height: number
weight: number
}
Union Types
Аналогічно перетину, ви можете виконати і об’єднання типів, тобто створити анотації кілька типів у поточній змінній. Дивіться приклад нижче:
type A = number
type B = string
type C = A | B
//Підсумок об'єднання A та B
type C = number или string
const parseAmount = (val: C) => {
if (typeof val === 'number') {
return val
}
if (typeof val === 'string') {
return val.resplace(',', '.')
}
}
Generic Types
Дженерики значно розширюють можливості TypeScript у повсякденному програмуванні, дозволяючи нам ефективно перевикористовувати типи, уникаючи створення безлічі аналогічних “клонів”. Давайте розглянемо це практичному прикладі: створення типу, здатного коректно обробляти відповіді методів.
type FetchResponse<T> = {
data: T
errorMessage: string
errorCode: number
}
type AuthDataRs = {
accessToken: string
refreshToken: string
}
const login = async (lg: string, ps: string): FetchResponse<AuthDataRs> => {
const response = await fetch(...)
return response
}
//FetchResponse<AuthDataRs> - вот такая запись позволит вам
//Перевикористовувати FetchResponse для різноманітних запитів.
При необхідності можна розширювати свій тип кількома дженериками:
type FetchResponse<T, P> = {
data: T
error: P
}
Також ви можете призначати тип за замовчуванням дженерику:
type FetchResponse<T, P = string> = {
data: T
error: P
}
Якщо дженеріку не призначений явний тип за промовчанням , то вам необхідно обов’язково вказувати тип при його використанні. У випадку, якщо у дженерика є дефолтне значення , передача типу може бути опціональною або навіть не потрібно.
Utility Types
Це утиліти, які призначені для зручної роботи, а саме створення нових типів на основі інших.
Awaited
Awaited<T>
Утиліта призначена для очікування в асинхронних операціях, наприклад:
type A = Awaited<Promise<number>>;
//type A -> number
Partial
Partial<T>
Утиліта призначена для створення нового типу, де кожна властивість стане опціональною. Нагадаю, для того, щоб зробити властивість об’єкта опціональним, необхідно використовувати знак “?”:
type A = {
id: number
name?: string //Опціональна властивість (необов'язкова)
}
Як працює Partial?
type A = {
id: number
name: string
}
type B = Partial<A>
//Output
type B = {
id?: number //Опціональна властивість (необов'язкова)
name?: number //Опціональна властивість (необов'язкова)
}
Required
Required<T>
Утиліта працює в точності навпаки як Partial. Властивості поточного типу робить обов’язковими.
type A = {
id?: number
name?: string
}
type B = Required<A>
//Output
type B = {
id: number //Обов'язкова властивість
name: number //Обов'язкова властивість
}
Readonly
Readonly<T>
Утиліти перетворює всі властивості типу, робить їх недоступними для перепризначення з використанням нового значення.
type A = {
id: number
name: string
}
type B = Readonly<A>
const firstObj: A = { id: 0, name: 'first'}
const secondObj: B = { id: 1, name: 'second'}
firstObj.name = 'first_1' // it's correct
secondObj.name = 'second_2' //Cannot assign to 'name' because it is a read-only property.
Якщо у вас є необхідність зробити поле лише тільки для певної властивості об’єкта, то необхідно написати ключове слово перед ім’ям св-ва:
type A = {
readonly id: number
name: string
}
Record
Record<T, U>
Утиліта призначена для створення типу об’єкта, Record<Keys, Types>, де Keys – імена властивостей об’єкта, а Types – типи значень властивостей.
enum CarNames {
AUDI = 'audi',
BMW = 'bmw'
}
type CarInfo = {
color: string
price: number
}
type Cars = Record<CarNames, CarInfo>
//Output
type Cars = {
audi: CarInfo;
bmw: CarInfo;
}
Pick
Pick<T, ‘key1’ | ‘key2’>
Утиліта призначена для створення нового типу із вибраних властивостей об’єкта.
type A = {
id: number
name: string
}
type B = Pick<A, 'name'>
//Output 1
type B = {
name: string
}
type B = Pick<A, 'id' | 'name'>
//Output 2
type B = {
id: number
name: string
}
Omit
Omit<T, ‘key1’ | ‘key2’>
Утиліта призначена для створення типу з решти (не виключених) властивостей об’єкта.
type A = {
id: number
name: string
}
type B = Omit<A, 'id'>
//Output 1
type B = {
name: string
}
type B = Omit<A, 'id' | 'name'>
//Output 2
type B2 = {}
Exclude
Exclude<T, U>
Утиліта створює тип, крім властивостей, які вже присутні у двох різних типах. Він виключає з T усі поля, які можна призначити U.
type A = {
id: number
name: string
length: number
}
type B = {
id: number
color: string
depth: string
}
type C = Exclude<keyof A, keyof B>
//Output
type C = "name" | "length"
Extract
Extract<T, U>
Створює тип, витягаючи з T всі члени об’єднання, які можна призначити U.
type A = {
id: number
name: string
length: number
}
type B = {
id: number
name: string
color: string
depth: string
}
type C = Extract<keyof A, keyof B>
//Output
type C = {
id: number
name: string
}
ReturnType
ReturnType<T>
Створює тип, що складається з типу, що повертається функцією T.
type A = () => string
type B = ReturnType<A>
//Output
type B = string
Це одні з основних Utility Types, у матеріалах до статті я залишу посилання на документацію, де за бажання ви зможете розібрати решту утиліт для просунутої роботи з TS.
Conditional Types
У TypeScript є можливість створювати типи в залежності від дженерика, що передається.
type ObjProps = {
id: number
name: string
}
type ExtendsObj<T> = T extends ObjProps ? ObjProps : T
const obj1: ObjProps = {
id: 0,
name: 'zero'
}
const obj2 = {
id: 1
}
type A = ExtendsObj<typeof obj1> // type A = ObjProps
type B = ExtendsObj<typeof obj2> // type B = { id: number }
Mapped Types
Порівняні типи дозволяють вам взяти існуючу модель і перетворити кожну з її властивостей на новий тип.
type MapToNumber<T> = {
[P in keyof T]: number
}
const obj = {id: 0, depth: '1005'}
type A = MapToNumber<typeof obj>
//Output
type A = {
id: number
depth: number
}
Type Guards
Якщо тип не визначений чи невідомий, то розробнику приходить “захист типів”.
typeof
Найпростіший спосіб убезпечити себе від помилки, безпосередньо перевірити тип за допомогою оператора typeof (раніше в прикладах ви могли бачити використання цього оператора, який повертає тип змінної).
const fn = (val: number | string) => {
if (typeof val === 'number') {
return ...
}
throw new Error(`Тип ${typeof val} не может быть обработан`)
}
in
Ще один із способів захистити тип, використовувати in, цей оператор перевіряє наявність властивості в об’єкті.
const obj = {
id: 1,
name: 'first'
}
const bool1 = 'name' in obj //true
const bool2 = 'foo' in obj //false
instanceof
Оператор екземпляра перевіряє, чи з’являється властивість прототипу конструктора десь у ланцюжку прототипів об’єкта
function C() {}
function D() {}
const o = new C();
o instanceof C //true
o instanceof D //false
is
Цей оператор вказує TypeScript який тип присвоїти змінній, якщо функція повертає true. У прикладі нижче оператор is звужує тип змінної foo (string | number) до string. Це певний користувачем захист типу. Завдяки захисту компілятор наводить тип до певного всередині блоку if.
interface FirstName {
firstName: string
}
interface FullName extends FirstName {
lastName: string
}
const isFirstName = (obj: any): obj is FirstName => {
return obj && typeof obj.firstName === "string"
}
const isFullName = (obj: any): obj is FullName => {
return isFirstName(obj) && typeof (obj as any).lastName === "string";
}
const testFn = (objInfo: FirstName | FullName | number) => {
if (isFullName(objInfo)) {
console.log('Тип FullName')
} else if (isFirstName(objInfo)) {
console.log('Тип FirstName')
} else {
console.log('Тип не принадлежит FullName или FirstName')
}
}
testFn({ firstName: 'Andrey' }) //Тип FirstName
testFn({ firstName: 'Andrey', lastName: 'Maslov' }) //Тип FullName
testFn(1) //Тип не належить FullName чи FirstName
Висновок
Як бачите, TypeScript – це потужний інструмент для розробки, який дозволяє покращити якість вашого коду, зробити його більш надійним та легко підтримуваним. У цьому туторіалі ми розглянули прийоми роботи з TypeScript над такими просунутими темами, наприклад, як дженерики та type guards.
Не забувайте, що вивчення TypeScript – це постійний процес, і чим більше ви практикуєтеся, тим впевненіше використовуватимете його у своїх проектах.
Якщо у вас виникнуть запитання або потрібна додаткова допомога, зверніться до офіційної документації TypeScript або додаткових матеріалів.