АвторМаслов Андрей, 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 в картинках

Комментарии (8)


  1. kacetal
    24.09.2023 11:29

    Что-то мне кажется что в Excludeу вас закралась ошибка, не хватает уникальных свойств типа В.


    1. Alexandroppolus
      24.09.2023 11:29

      Да нет, всё там правильно. Exclude<A, B> - это "все элементы из А, которых не нашлось в В"


      1. kacetal
        24.09.2023 11:29

        Спасибо, значит это я не с первого раза понял.


  1. NeoCode
    24.09.2023 11:29
    +3

    Интересно. Пересечение типов больше похоже на теоретико-множественное объединение, а объединение типов - это скорее "тип-сумма".


    1. Alexandroppolus
      24.09.2023 11:29
      +2

      Пересечение типов больше похоже на теоретико-множественное объединение

      Это так выглядит для объектных типов. Объект соответствует заданному типу, если у него есть указанные в типе поля, и неважно, имеются ли какие-то другие. Вот так и выходит, что объект, одновременно соответствующий типам A = {a: T1, c: T3} и B = {b: T2, c: T4}, должен соответствовать типу {a: T1, b: T2, c: T3 & T4}. Так что на самом деле там теоретико-множественное пересечение в чистом виде.


    1. mayorovp
      24.09.2023 11:29

      Про пересечение типов вам уже сказали, скажу про объединение.


      Принципиальное отличие типа-объединения от типа-суммы — в том, что T | T = T, но T + T != T. Они решают одну и ту же задачу, и зачастую случаев взаимозаменимы, но их свойства всё-таки отличаются.


  1. dglazkov
    24.09.2023 11:29
    +1

    Exclude<T, U>

    Утилита создает тип, исключая свойства, которые уже присутствуют в двух разных типах. Он исключает из T все поля, которые можно назначить U.

    В документации написано по другому

    Constructs a type by excluding from UnionType all union members that are assignable to ExcludedMembers.

    Речь идет именно про то, что эта утилита Exclude помогает работать с UnionType. В примведенном примере и описании к Exclude этого совсем не упоминается, и даже ноборот запутывает. Появляется вопрос, а зачем использовать Exclude, если я тоже самое могу сделать с помощью Omit: Omit<A, keyof B> .


  1. NN1
    24.09.2023 11:29
    +3

    Зная вышеперечисленное несложно решать Type Challenges :)