Привет, друзья!
Представляю вашему вниманию перевод 2 статей:
- Use TypeScript Mapped Types Like a Pro о связанных или сопоставленных типах (mapped types)
TypeScript
; - 10 TypeScript features you might not be using yet or didn't understand о полезных возможностях современного
TS
.
Связанные типы
Приходилось ли вам использовать вспомогательные типы Partial
, Required
, Readonly
и Pick
?
Интересно, как они реализованы?
Регистрация пользователей является распространенной задачей в веб-разработке. Определим тип User
, в котором все ключи являются обязательными:
type User = {
name: string
password: string
address: string
phone: string
}
Как правило, зарегистрированные пользователи могут модифицировать некоторые данные о себе. Определим новый тип PartialUser
, в котором все ключи являются опциональными:
type PartialUser = {
name?: string
password?: string
address?: string
phone?: string
}
В отдельных случаях требуется, чтобы все ключи были доступными только для чтения. Определим новый тип ReadonlyUser
:
type ReadonlyUser = {
readonly name: string
readonly password: string
readonly address: string
readonly phone: string
}
Получаем много дублирующегося кода:
Как можно уменьшить его количество? Ответ — использовать сопоставленные типы, которые являются общими типами (generic types), позволяющими связывать тип исходного объекта с типом нового объекта.
Синтаксис связанных типов:
P in K
можно сравнить с инструкцией for..in
в JavaScript
, она используется для перебора всех ключей типа K
. Тип переменной T
— это любой тип, валидный с точки зрения TS
.
В процессе связывания типов могут использоваться дополнительные модификаторы, такие как readonly
и ?
. Соответствующие модификаторы добавляются и удаляются с помощью символов +
и -
. По умолчанию модификатор добавляется.
Синтаксис основных связанных типов:
{ [ P in K ] : T }
{ [ P in K ] ?: T }
{ [ P in K ] -?: T }
{ readonly [ P in K ] : T }
{ readonly [ P in K ] ?: T }
{ -readonly [ P in K ] ?: T }
Несколько примеров:
Переопределим тип PartialUser
с помощью связанного типа:
type MyPartial<T> = {
[P in keyof T]?: T[P]
}
type PartialUser = MyPartial<User>
MyPartial
используется для сопоставления типов User
и PartialUser
. Оператор keyof
возвращает все ключи типа в виде объединения (union type). Тип переменной P
меняется на каждой итерации. T[P]
используется для получения типа значения, соответствующего атрибуту типа объекта.
Демонстрация потока выполнения MyPartial
:
TS 4.1
позволяет повторно связывать ключи связанных типов с помощью ключевого слова as
. Синтаксис выглядит так:
type MappedTypeWithNewKeys<T> = {
[K in keyof T as NewKeyType]: T[K]
// ^^^^^^^^^^^^^
}
Тип NewKeyType
должен быть подтипом объединения string | number | symbol
. as
позволяет определить вспомогательный тип, генерирующий соответствующие геттеры для объектного типа:
type Getters<T> = {
[K in keyof T as `get${Capitalize<string & K>}`]: () => T[K]
}
interface Person {
name: string
age: number
location: string
}
type LazyPerson = Getters<Person>
// {
// getName: () => string
// getAge: () => number
// getLocation: () => string
// }
Поскольку тип, возвращаемый keyof T
может содержать тип symbol
, а вспомогательный тип Capitalize
требует, чтобы обрабатываемый тип был подтипом string
, фильтрация типов с помощью оператора &
в данном случае является обязательной.
Повторно связываемые ключи можно фильтровать путем возвращения типа never
:
// Удаляем свойство 'kind'
type RemoveKindField<T> = {
[K in keyof T as Exclude<K, 'kind'>]: T[K]
}
interface Circle {
kind: 'circle'
radius: number
}
type KindlessCircle = RemoveKindField<Circle>
// type KindlessCircle = {
// radius: number
// }
Тип unknown
Тип unknown
является безопасной (с точки зрения типов) версией типа any
.
Начнем с того, что между ними общего.
Переменным с типами any
и unknown
можно присваивать любые значения:
let anyValue: any = 'any value'
anyValue = true
anyValue = 123
console.log(anyValue) // 123
let unknownValue: unknown
unknownValue = 'unknown value'
unknownValue = true
unknownValue = 123
console.log(unknownValue) // 123
Однако, в отличие от any
, unknown
не позволяет оперировать "неизвестными" значениями.
Пример:
const anyValue: any = 'any value'
console.log(anyValue.add())
Еще один:
const anyValue: any = true
anyValue.typescript.will.not.complain.about.this.method()
В обоих случаях получаем ошибку времени выполнения (только во время выполнения кода).
При использовании any
мы говорим компилятору: "Не проверяй этот код — я знаю, что делаю". Компилятор TS
, в свою очередь, не делает никаких предположений относительно типа. Это не позволяет предотвращать ошибки на этапе компиляции кода.
Тип unknown
позволяет заранее обнаруживать ошибки времени выполнения (на стадии компиляции). Более того, многие IDE
обеспечивают подсветку потенциальных ошибок такого рода.
Рассмотрим несколько примеров:
let unknownValue: unknown
unknownValue = 'unknown value'
unknownValue.toString() // Ошибка: Object is of type 'unknown'.
unknownValue = 100
const value = unknownValue + 10 // Ошибка: Object is of type 'unknown'.
unknownValue = console
unknownValue.log('test') // Ошибка: Object is of type 'unknown'.
При выполнении любой операции над неизвестным значением возникает ошибка.
Для того, чтобы манипулировать таким значением, требуется произвести сужение типа (type narrowing). Это можно сделать несколькими способами.
С помощью утверждения типа (type assertion):
let unknownValue: unknown
unknownValue = function () {}
;(unknownValue as Function).call(null)
С помощью предохранителя типа (type guard):
let unknownValue: unknown
unknownValue = 'unknown value'
if (typeof unknownValue === 'string') unknownValue.toString()
С помощью кастомного предохранителя типа:
let unknownValue: unknown
type User = { username: string }
function isUser(maybeUser: any): maybeUser is User {
return 'username' in maybeUser
}
unknownValue = { username: 'John' }
if (isUser(unknownValue)) {
console.log(unknownValue.username)
}
С помощью функции-утверждения (assertion function):
let unknownValue: unknown
unknownValue = 123
function isNumber(value: unknown): asserts value is number {
if (typeof value !== 'number') throw Error('Переданное значение не является числом!')
}
isNumber(unknownValue)
unknownValue.toFixed()
Индексированный тип доступа (тип поиска)
Индексированный тип доступа (indexed access type) может использоваться для поиска определенного свойства в другом типе. Рассмотрим пример:
type User = {
id: number
username: string
email: string
address: {
city: string
state: string
country: string
postalCode: number
}
addons: { name: string, id: number }[]
}
type Id = User['id'] // number
type Session = User['address']
// {
// city: string
// state: string
// country: string
// postalCode: string
// }
type Street = User['address']['state'] // string
type Addons = User['addons'][number]
// {
// name: string
// id: number
// }
Здесь мы создаем новые типы Id
, Session
, Street
и Addons
на основе существующего объекта. Индексированный тип доступа можно использовать прямо в функции:
function printAddress(address: User['address']): void {
console.log(`
city: ${address.city},
state: ${address.state},
country: ${address.country},
postalCode: ${address.postalCode}
`)
}
Ключевое слово infer
Ключевое слово infer
позволяет выводить один тип из другого внутри условного типа. Пример:
const User = {
id: 123,
username: 'John',
email: 'john@mail.com',
addons: [
{ name: 'First addon', id: 1 },
{ name: 'Second addon', id: 2 }
]
}
type UnpackArray<T> = T extends (infer R)[] ? R : T
type AddonType = UnpackArray<typeof User.addons> // { name: string, id: number }
Тип addon
выделен в отдельный тип.
Функции-утверждения
Существует специальный набор функций, выбрасывающих исключения, когда происходит что-либо неожиданное. Такие функции называются "функциями-утверждениями" (assertion functions):
function assertIsString(value: unknown): asserts value is string {
if (typeof value !== 'string') throw Error('Переданное значение не является строкой!')
}
Пример проверки объектов, включая вложенные:
type User = {
id: number
username: string
email: string
address: {
city: string
state: string
country: string
postalCode: number
}
}
function assertIsObject(obj: unknown, errorMessage: string = 'Неправильный объект!'): asserts obj is object {
if(typeof obj !== 'object' || obj === null) throw new Error(errorMessage)
}
function assertIsAddress(address: unknown): asserts address is User['address'] {
const errorMessage = 'Неправильны адрес!'
assertIsObject(address, errorMessage)
if(
!('city' in address) ||
!('state' in address) ||
!('country' in address) ||
!('postalCode' in address)
) throw new Error(errorMessage)
}
function assertIsUser(user: unknown): asserts user is User {
const errorMessage = 'Неправильный пользователь!'
assertIsObject(user, errorMessage)
if(
!('id' in user) ||
!('username' in user) ||
!('email' in user)
) throw new Error(errorMessage)
assertIsAddress((user as User).address)
}
Пример использования:
class UserWebService {
static getUser = (id: number): User | unknown => undefined
}
const user = UserWebService.getUser(123)
assertIsUser(user) // Если пользователь является "неправильным", выбрасывается исключение
user.address.postalCode // На данном этапе мы знаем, что `user` - валидный объект
Тип never
Тип never
является индикатором того, что функция выбрасывает исключение или прерывает выполнение программы:
function plus1If1(value: number): number | never {
if(value === 1) return value + 1
throw new Error('Ошибка!')
}
Пример с промисом:
const promise = (value: number) => new Promise<number | never>((resolve, reject) => {
if(value === 1) resolve(1 + value)
reject(new Error('Ошибка!'))
})
И еще один:
function infiniteLoop(): never {
while (true) {
// ...
}
}
never
также возникает в случае, когда в объединение не осталось "свободных" типов:
function valueCheck(value: string | number) {
if (typeof value === "string") {
// значение является строкой
} else if (typeof value === "number") {
// значение является числом
} else {
// значение имеет тип `never`
}
}
Утверждение const
Использование const
при конструировании новых буквальных выражений (literal expressions) сообщает компилятору следующее:
- типы выражения не должны расширяться (например, тип
привет
не может конвертироваться вstring
); - свойства объектных литералов становятся доступными только для чтения (
readonly
); - литералы массивов становятся доступными только для чтения кортежами (tuples).
Пример:
const email = 'john@mail.com' // john@mail.com
const phones = [89876543210, 89123456780] // number[]
const session = { id: 123, name: 'qwerty123456' }
// {
// id: number
// name: string
// }
const username = 'John' as const // John
const roles = [ 'read', 'write'] as const // readonly ["read", "write"]
const address = { street: 'Tokyo', country: 'Japan' } as const
// {
// readonly street: "Tokyo"
// readonly country: "Japan"
// }
const user = {
email,
phones,
session,
username,
roles,
address
} as const
// {
// readonly email: "john@mail.com"
// readonly phones: number[]
// readonly session: {
// id: number
// name: string
// }
// readonly username: "John"
// readonly roles: readonly ["read", "write"]
// readonly address: {
// readonly street: "Tokyo"
// readonly country: "Japan"
// }
// }
// С утверждением `const`
// user.email = 'jane@mail.com' // Ошибка
// user.phones = [] // Ошибка
user.phones.push(89087654312)
// user.session = { name: 'test4321', id: 124 } // Ошибка
user.session.name = 'test4321'
// С "внешним" и "внутренним" утверждениями `const`
// user.username = 'Jane' // Ошибка
// user.roles.push('ReadAndWrite') // Ошибка
// user.address.city = 'Osaka' // Ошибка
Утверждение const
позволяет преобразовывать массив строк в объединение:
const roles = ['read', 'write', 'readAndWrite'] as const
type Roles = typeof roles[number]
// Эквивалентно
// type Roles = "read" | "write" | "readAndWrite"
type RolesInCapital = Capitalize<typeof roles[number]>
// Эквивалентно
// type RolesInCapital = "Read" | "Write" | "ReadAndWrite"
[number]
указывает TS
извлечь все числовые индексированные значения из массива roles
.
Ключевое слово override
Ключевое слово override
может использоваться для обозначения перезаписываемого метода дочернего класса (начиная с TS 4.3
):
class Employee {
doWork() {
console.log('Я работаю')
}
}
class Developer extends Employee {
override doWork() {
console.log('Я программирую')
}
}
Вот как сделать эту "фичу" обязательной:
{
"compilerOptions": {
"noImplicitOverride": true
}
}
Блок static
static
позволяет определять блоки инициализации классов (начиная с TS 4.4
):
function userCount() {
return 10
}
class User {
id = 0
static count: number = 0
constructor(
public username: string,
public age: number
) {
this.id = ++User.count
}
static {
User.count += userCount()
}
}
console.log(User.count) // 10
new User('John', 32)
new User('Jane', 23)
console.log(User.count) // 12
Статические индексы и индексы экземпляров
Сигнатура индекса (index signature) позволяет устанавливать больше свойств, чем изначально определено в типе:
class User {
username: string
age: number
constructor(username: string,age: number) {
this.username = username
this.age = age
}
[propName: string]: string | number
}
const user = new User('John', 23)
user['phone'] = '+79876543210'
const something = user['something']
Пример использования сигнатуры статического индекса:
class User {
username: string
age: number
constructor(username: string,age: number) {
this.username = username
this.age = age
}
static [propName: string]: string | number
}
User['userCount'] = 0
const something = User['something']
Пожалуй, это все, чем я хотел поделиться с вами в этой статье. Надеюсь, вы нашли для себя что-то интересное и не зря потратили время.
Благодарю за внимание и happy coding!