АвторМаслов Андрей, Front-end разработчик.

О статье

Эта статья создана для облегчения процесса изучения 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.

Если у вас есть необходимость сделать поле readonly только для определенного свойства объекта, то необходимо написать ключевое слово перед именем св-ва:

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>
Утилита создает union тип, исключая свойства, которые уже присутствуют в двух разных типах. Он исключает из 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>
Создает union тип, извлекая из 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" | "name"

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 или дополнительным материалам.

Материалы для изучения

Основы
Utility Types
Шпаргалка по TS в картинках