Автор: Маслов Андрей, 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 или дополнительным материалам.
Материалы для изучения
Комментарии (8)
NeoCode
24.09.2023 11:29+3Интересно. Пересечение типов больше похоже на теоретико-множественное объединение, а объединение типов - это скорее "тип-сумма".
Alexandroppolus
24.09.2023 11:29+2Пересечение типов больше похоже на теоретико-множественное объединение
Это так выглядит для объектных типов. Объект соответствует заданному типу, если у него есть указанные в типе поля, и неважно, имеются ли какие-то другие. Вот так и выходит, что объект, одновременно соответствующий типам A = {a: T1, c: T3} и B = {b: T2, c: T4}, должен соответствовать типу {a: T1, b: T2, c: T3 & T4}. Так что на самом деле там теоретико-множественное пересечение в чистом виде.
mayorovp
24.09.2023 11:29Про пересечение типов вам уже сказали, скажу про объединение.
Принципиальное отличие типа-объединения от типа-суммы — в том, что
T | T = T
, ноT + T != T
. Они решают одну и ту же задачу, и зачастую случаев взаимозаменимы, но их свойства всё-таки отличаются.
dglazkov
24.09.2023 11:29+1Exclude<T, U>
Утилита создает тип, исключая свойства, которые уже присутствуют в двух разных типах. Он исключает из T все поля, которые можно назначить U.
В документации написано по другому
Constructs a type by excluding from
UnionType
all union members that are assignable toExcludedMembers
.Речь идет именно про то, что эта утилита
Exclude
помогает работать сUnionType
. В примведенном примере и описании кExclude
этого совсем не упоминается, и даже ноборот запутывает. Появляется вопрос, а зачем использоватьExclude
, если я тоже самое могу сделать с помощьюOmit: Omit<A, keyof B>
.
kacetal
Что-то мне кажется что в
Exclude
у вас закралась ошибка, не хватает уникальных свойств типа В.Alexandroppolus
Да нет, всё там правильно. Exclude<A, B> - это "все элементы из А, которых не нашлось в В"
kacetal
Спасибо, значит это я не с первого раза понял.