TypeScript і все, що тобі потрібно в розробці

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 або додаткових матеріалів.

Джерело