TypeScript и все что тебе нужно в разработке
- понедельник, 25 сентября 2023 г. в 00:00:15
Автор: Маслов Андрей, Front-end разработчик.
Эта статья создана для облегчения процесса изучения TypeScript с помощью практичных примеров. Более подробную информацию можно найти в документации или в дополнительных материалах.
Статья предназначена как для начинающих разработчиков, которые только начинают знакомиться с TypeScript, так и для опытных разработчиков, желающих углубить свои знания в этом языке. Здесь вы найдете краткое и информативное изложение ключевых аспектов TypeScript, которые могут быть полезными в повседневной разработке. Для вашего удобства, оглавление статьи содержит ссылки на конкретные темы TypeScript, так что вы можете быстро перейти к интересующей вас части материала.
В 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
}
Аналогично пересечению, вы можете выполнить и объединение типов, т.е создать аннотации несколько типов в текущей переменной. Смотрите пример ниже:
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(',', '.')
}
}
Дженерики значительно расширяют возможности 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
}
Если дженерику не назначен явный тип по умолчанию, то вам необходимо обязательно указывать тип при его использовании. В случае, если у дженерика есть дефолтное значение, передача типа может быть опциональной или даже вовсе не требоваться.
Это утилиты, которые предназначены для удобной работы, а именно генерации новых типов на основе других.
Awaited<T>
Утилита предназначена для ожидания в асинхронных операциях, например:
type A = Awaited<Promise<number>>;
//type A -> number
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<T>
Утилита работает в точности наоборот как Partial. Свойства текущего типа делает строго обязательными.
type A = {
id?: number
name?: string
}
type B = Required<A>
//Output
type B = {
id: number //Обязательное свойство
name: number //Обязательное свойство
}
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<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<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<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<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<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<T>
Создает тип, состоящий из типа, возвращаемого функцией T.
type A = () => string
type B = ReturnType<A>
//Output
type B = string
Это одни из основных Utility Types, в материалах к статье я оставлю ссылку на документацию, где при желании вы сможете разобрать остальные утилиты для продвинутой работы с TS.
В 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 }
Сопоставленные типы позволяют вам взять существующую модель и преобразовать каждое из ее свойств в новый тип.
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
}
Если тип не определен или неизвестен, то на помощь разработчику приходит "защита типов".
Самый простой способ обезопасить себя от ошибки, напрямую проверить тип при помощи оператора typeof (ранее в примерах вы могли видеть использование этого оператора, который возвращает тип переменной).
const fn = (val: number | string) => {
if (typeof val === 'number') {
return ...
}
throw new Error(`Тип ${typeof val} не может быть обработан`)
}
Еще один из способов защитить тип, использовать in, этот оператор проверяет присутствие свойства в объекте.
const obj = {
id: 1,
name: 'first'
}
const bool1 = 'name' in obj //true
const bool2 = 'foo' in obj //false
Оператор экземпляра проверяет, появляется ли свойство прототипа конструктора где-нибудь в цепочке прототипов объекта
function C() {}
function D() {}
const o = new C();
o instanceof C //true
o instanceof D //false
Этот оператор указывает 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 или дополнительным материалам.