
Автор: Маслов Андрей, 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 или дополнительным материалам.
