Привет, друзья!


Представляю вашему вниманию перевод 2 статей:



Связанные типы


Приходилось ли вам использовать вспомогательные типы 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!




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


  1. ht-pro
    17.08.2022 12:11
    +1

    Спасибо за перевод.


  1. md_backend_binance
    17.08.2022 23:20

    как же я хочу это в C# протащить в виде анонимных деструкторов обобщённых типов