Мы продолжаем серию публикаций адаптированного и дополненного перевода "Карманной книги по TypeScript
".
Другие части:
- Часть 1. Основы
- Часть 2. Типы на каждый день
- Часть 3. Сужение типов
- Часть 4. Подробнее о функциях
- Часть 5. Объектные типы
- Часть 6. Манипуляции с типами
- Часть 7. Классы
- Часть 8. Модули
Обратите внимание: для большого удобства в изучении книга была оформлена в виде прогрессивного веб-приложения.
Система типов TS
позволяет создавать типы на основе других типов.
Простейшей формой таких типов являются дженерики или общие типы (generics). В нашем распоряжении также имеется целый набор операторов типа. Более того, мы можем выражать типы в терминах имеющихся у нас значений.
Дженерики
Создадим функцию identity
, которая будет возвращать переданное ей значение:
function identity(arg: number): number {
return arg
}
Для того, чтобы сделать эту функцию более универсальной, можно использовать тип any
:
function identity(arg: any): any {
return arg
}
Однако, при таком подходе мы не будем знать тип возвращаемого функцией значения.
Нам нужен какой-то способ перехватывать тип аргумента для обозначения с его помощью типа возвращаемого значения. Для этого мы можем воспользоваться переменной типа, специальным видом переменных, которые работают с типами, а не со значениями:
function identity<Type>(arg: Type): Type {
return arg
}
Мы используем переменную Type
как для типа передаваемого функции аргумента, так и для типа возвращаемого функцией значения.
Такие функции называют общими (дженериками), поскольку они могут работать с любыми типами.
Мы можем вызывать такие функции двумя способами. Первый способ заключается в передаче всех аргументов, включая аргумент типа:
const output = identity<string>('myStr')
// let output: string
В данном случае принимаемым и возвращаемым типами является строка.
Второй способ заключается в делегировании типизации компилятору:
const output = identity('myStr')
// let output: string
Второй способ является более распространенным. Однако, в более сложных случаях может потребоваться явное указание типа, как в первом примере.
Работа с переменными типа в дженериках
Что если мы захотим выводить в консоль длину аргумента arg
перед его возвращением?
function loggingIdentity<Type>(arg: Type): Type {
console.log(arg.length)
// Property 'length' does not exist on type 'Type'.
// Свойства 'length' не существует в типе 'Type'
return arg
}
Мы получаем ошибку, поскольку переменные типа указывают на любой (а, значит, все) тип, следовательно, аргумент arg
может не иметь свойства length
, например, если мы передадим в функцию число.
Изменим сигнатуру функции таким образом, чтобы она работала с массивом Type
:
function loggingIdentity<Type>(arg: Type[]): Type[] {
console.log(arg.length)
return arg
}
Теперь наша функция стала дженериком, принимающим параметр Type
и аргумент arg
, который является массивом Type
, и возвращает массив Type
. Если мы передадим в функцию массив чисел, то получим массив чисел.
Мы можем сделать тоже самое с помощью такого синтаксиса:
function loggingIdentity<Type>(arg: Array<Type>): Array<Type> {
console.log(arg.length)
return arg
}
Общие типы
Тип общей функции (функции-дженерика) похож на тип обычной функции, в начале которого указывается тип параметра:
function identity<Type>(arg: Type): Type {
return arg
}
const myIdentity: <Type>(arg: Type) => Type = identity
Мы можем использовать другое название для параметра общего типа:
function identity<Type>(arg: Type): Type {
return arg
}
const myIdentity: <Input>(arg: Input) => Input = identity
Мы также можем создавать общие типы в виде сигнатуры вызова типа объектного литерала:
function identity<Type>(arg: Type): Type {
return arg
}
const myIdentity: { <Type>(arg: Type): Type } = identity
Это приводит нас к общему интерфейсу:
interface GenericIdentityFn {
<Type>(arg: Type): Type
}
function identity<Type>(arg: Type): Type {
return arg
}
const myIdentity: GenericIdentityFn = identity
Для того, чтобы сделать общий параметр видимым для всех членов интерфейса, его необходимо указать после названия интерфейса:
interface GenericIdentityFn<Type> {
(arg: Type): Type
}
function identity<Type>(arg: Type): Type {
return arg
}
const myIdentity: GenericIdentityFn<number> = identity
Кроме общих интерфейсов, мы можем создавать общие классы.
Обратите внимание, что общие перечисления (enums) и пространства имен (namespaces) создавать нельзя.
Общие классы
Общий класс имеет такую же форму, что и общий интерфейс:
class GenericNumber<NumType> {
zeroValue: NumType
add: (x: NumType, y: NumType) => NumType
}
const myGenericNum = new GenericNumber<number>()
myGenericNum.zeroValue = 0
myGenericNum.add = (x, y) => x + y
В случае с данным классом мы не ограничены числами. Мы вполне можем использовать строки или сложные объекты:
const stringNumeric = new GenericNumber<string>()
stringNumeric.zeroValue = ''
stringNumeric.add = (x, y) => x + y
console.log(stringNumeric.add(stringNumeric.zeroValue, 'test'))
Класс имеет две стороны с точки зрения типов: статическую сторону и сторону экземпляров. Общие классы являются общими только для экземпляров. Это означает, что статические члены класса не могут использовать тип параметра класса.
Ограничения дженериков
Иногда возникает необходимость в создании дженерика, работающего с набором типов, когда мы имеем некоторую информацию о возможностях, которыми будет обладать этот набор. В нашем примере loggingIdentity
мы хотим получать доступ к свойству length
аргумента arg
, но компилятор знает, что не каждый тип имеет такое свойство, поэтому не позволяет нам делать так:
function loggingIdentity<Type>(arg: Type): Type {
console.log(arg.length)
// Property 'length' does not exist on type 'Type'.
return arg
}
Мы хотим, чтобы функция работала с любым типом, у которого имеется свойство length
. Для этого мы должны создать ограничение типа.
Нам необходимо создать интерфейс, описывающий ограничение. В следующем примере мы создаем интерфейс с единственным свойством length
и используем его с помощью ключевого слова extends
для применения органичения:
interface Lengthwise {
length: number
}
function loggingIdentity<Type extends Lengthwise>(arg: Type): Type {
console.log(arg.length)
// Теперь мы можем быть увереными в существовании свойства `length`
return arg
}
Поскольку дженерик был ограничен, он больше не может работать с любым типом:
loggingIdentity(3)
// Argument of type 'number' is not assignable to parameter of type 'Lengthwise'.
// Аргумент типа 'number' не может быть присвоен параметру типа 'Lengthwise'
Мы должны передавать ему значения, отвечающие всем установленным требованиям:
loggingIdentity({ length: 10, value: 3 })
Использование типов параметров в ограничениях дженериков
Мы можем определять типы параметров, ограниченные другими типами параметров. В следующем примере мы хотим получать свойства объекта по их названиям. При этом, мы хотим быть уверенными в том, что не извлекаем несуществующих свойств. Поэтому мы помещаем ограничение между двумя типами:
function getProperty<Type, Key extends keyof Type>(obj: Type, key: Key) {
return obj[key]
}
const x = { a: 1, b: 2, c: 3, d: 4 }
getProperty(x, 'a')
getProperty(x, 'm')
// Argument of type '"m"' is not assignable to parameter of type '"a" | "b" | "c" | "d"'.
Использование типов класса в дженериках
При создании фабричных функций с помощью дженериков, необходимо ссылаться на типы классов через их функции-конструкторы. Например:
function create<Type>(c: { new (): Type }): Type {
return new c()
}
В более сложных случаях может потребоваться использование свойства prototype
для вывода и ограничения отношений между функцией-конструктором и стороной экземляров типа класса:
class BeeKeeper {
hasMask: boolean
}
class ZooKeeper {
nametag: string
}
class Animal {
numLegs: number
}
class Bee extends Animal {
keeper: BeeKeeper
}
class Lion extends Animal {
keeper: ZooKeeper
}
function createInstance<A extends Animal>(c: new () => A): A {
return new c()
}
createInstance(Lion).keeper.nametag
createInstance(Bee).keeper.hasMask
Данный подход часто используется в миксинах или примесях.
Оператор типа keyof
Оператор keyof
"берет" объектный тип и возвращает строковое или числовое литеральное объединение его ключей:
type Point = { x: number, y: number }
type P = keyof Point
// type P = keyof Point
Если типом сигнатуры индекса (index signature) типа является string
или number
, keyof
возвращает эти типы:
type Arrayish = { [n: number]: unknown }
type A = keyof Arrayish
// type A = number
type Mapish = { [k: string]: boolean }
type M = keyof Mapish
// type M = string | number
Обратите внимание, что типом M
является string | number
. Это объясняется тем, что ключи объекта в JS
всегда преобразуются в строку, поэтому obj[0]
— это всегда тоже самое, что obj['0']
.
Типы keyof
являются особенно полезными в сочетании со связанными типами (mapped types), которые мы рассмотрим позже.
Оператор типа typeof
JS
предоставляет оператор typeof
, который можно использовать в контексте выражения:
console.log(typeof 'Привет, народ!') // string
В TS
оператор typeof
используется в контексте типа для ссылки на тип переменной или свойства:
const s = 'привет'
const n: typeof s
// const n: string
В сочетании с другими операторами типа мы можем использовать typeof
для реализации нескольких паттернов. Например, давайте начнем с рассмотрения предопределенного типа ReturnType<T>
. Он принимает тип функции и производит тип возвращаемого функцией значения:
type Predicate = (x: unknown) => boolean
type K = ReturnType<Predicate>
// type K = boolean
Если мы попытаемся использовать название функции в качестве типа параметра ReturnType
, то получим ошибку:
function f() {
return { x: 10, y: 3 }
}
type P = ReturnType<f>
// 'f' refers to a value, but is being used as a type here. Did you mean 'typeof f'?
// 'f' является ссылкой на значение, но используется как тип. Возможно, вы имели ввиду 'typeof f'
Запомните: значения и типы — это не одно и тоже. Для ссылки на тип значения f
следует использовать typeof
:
function f() {
return { x: 10, y: 3 }
}
type P = ReturnType<typeof f>
// type P = { x: number, y: number }
Ограничения
TS
ограничивает виды выражений, на которых можно использовать typeof
.
typeof
можно использовать только в отношении идентификаторов (названий переменных) или их свойств. Это помогает избежать написания кода, который не выполняется:
// Должны были использовать ReturnType<typeof msgbox>, но вместо этого написали
const shouldContinue: typeof msgbox('Вы уверены, что хотите продолжить?')
// ',' expected
Типы доступа по индексу (indexed access types)
Мы можем использовать тип доступа по индексу для определения другого типа:
type Person = { age: number, name: string, alive: boolean }
type Age = Person['age']
// type Age = number
Индексированный тип — это обычный тип, так что мы можем использовать объединения, keyof
и другие типы:
type I1 = Person['age' | 'name']
// type I1 = string | number
type I2 = Person[keyof Person]
// type I2 = string | number | boolean
type AliveOrName = 'alive' | 'name'
type I3 = Person[AliveOrName]
// type I3 = string | boolean
При попытке доступа к несуществующему свойству возникает ошибка:
type I1 = Person['alve']
// Property 'alve' does not exist on type 'Person'.
Другой способ индексации заключается в использовании number
для получения типов элементов массива. Мы также можем использовать typeof
для перехвата типа элемента:
const MyArray = [
{ name: 'Alice', age: 15 },
{ name: 'Bob', age: 23 },
{ name: 'John', age: 38 },
]
type Person = typeof MyArray[number]
type Person = {
name: string
age: number
}
type Age = typeof MyArray[number]['age']
type Age = number
// или
type Age2 = Person['age']
type Age2 = number
Обратите внимание, что мы не можем использовать const
, чтобы сослаться на переменную:
const key = 'age'
type Age = Person[key]
/*
Type 'any' cannot be used as an index type.
'key' refers to a value, but is being used as a type here. Did you mean 'typeof key'?
*/
/*
Тип 'any' не может быть использован в качестве типа индекса.
'key' является ссылкой на значение, но используется как тип. Возможно, вы имели ввиду 'typeof key'
*/
Однако, в данном случае мы можем использовать синоним типа (type alias):
type key = 'age'
type Age = Person[key]
Условные типы (conditional types)
Обычно, в программе нам приходится принимать решения на основе некоторых входных данных. В TS
решения также зависят от типов передаваемых аргументов. Условные типы помогают описывать отношения между типами входящих и выходящих данных.
interface Animal {
live(): void
}
interface Dog extends Animal {
woof(): void
}
type Example1 = Dog extends Animal ? number : string
// type Example1 = number
type Example2 = RegExp extends Animal ? number : string
// type Example2 = string
Условные типы имеют форму, схожую с условными выражениями в JS
(условие ? истинноеВыражение : ложноеВыражение
).
SomeType extends OtherType ? TrueType : FalseType
Когда тип слева от extends
может быть присвоен типу справа от extends
, мы получаем тип из первой ветки (истинной), в противном случае, мы получаем тип из второй ветки (ложной).
В приведенном примере польза условных типов не слишком очевидна. Она становится более явной при совместном использовании условных типов и дженериков (общих типов).
Рассмотрим такую функцию:
interface IdLabel {
id: number /* некоторые поля */
}
interface NameLabel {
name: string /* другие поля */
}
function createLabel(id: number): IdLabel
function createLabel(name: string): NameLabel
function createLabel(nameOrId: string | number): IdLabel | NameLabel
function createLabel(nameOrId: string | number): IdLabel | NameLabel {
throw 'не реализовано'
}
Перегрузки createLabel
описывают одну и ту же функцию, которая делает выбор на основе типов входных данных.
Обратите внимание на следующее:
- Если библиотека будет выполнять такую проверку снова и снова, это будет не очень рациональным.
- Нам пришлось создать 3 перегрузки: по одной для каждого случая, когда мы уверены в типе (одну для
string
и одну дляnumber
), и еще одну для общего случая (string
илиnumber
). Количество перегрузок будет увеличиваться пропорционально добавлению новых типов.
Вместо этого, мы можем реализовать такую же логику с помощью условных типов:
type NameOrId<T extends number | string> = T extends number
? IdLabel
: NameLabel
Затем мы можем использовать данный тип для избавления от перегрузок:
function createLabel<T extends number | string>(idOrName: T): NameOrId<T> {
throw 'не реализовано'
}
let a = createLabel('typescript')
// let a: NameLabel
let b = createLabel(2.8)
// let b: IdLabel
let c = createLabel(Math.random() ? 'hello' : 42)
// let c: NameLabel | IdLabel
Ограничения условных типов
Часто проверка в условном типе дает нам некоторую новую информацию. Подобно тому, как сужение с помощью защитников или предохранителей типа (type guards) возвращает более конкретный тип, инстинная ветка условного типа ограничивает дженерики по типу, который мы проверяем.
Рассмотрим такой пример:
type MessageOf<T> = T['message']
// Type '"message"' cannot be used to index type 'T'.
// Тип '"message"' не может быть использован для индексации типа 'T'
В данном случае возникает ошибка, поскольку TS
не знает о существовании у T
свойства message
. Мы можем ограничить T
, и тогда TS
перестанет "жаловаться":
type MessageOf<T extends { message: unknown }> = T['message']
interface Email {
message: string
}
interface Dog {
bark(): void
}
type EmailMessageContents = MessageOf<Email>
// type EmailMessageContents = string
Но что если мы хотим, чтобы MessageOf
принимал любой тип, а его "дефолтным" значением был тип never
? Мы можем "вынести" ограничение и использовать условный тип:
type MessageOf<T> = T extends { message: unknown } ? T['message'] : never
interface Email {
message: string
}
interface Dog {
bark(): void
}
type EmailMessageContents = MessageOf<Email>
// type EmailMessageContents = string
type DogMessageContents = MessageOf<Dog>
// type DogMessageContents = never
Находясь внутри истинной ветки, TS
будет знать, что T
имеет свойство message
.
В качестве другого примера мы можем создать тип Flatten
, который распаковывает типы массива на типы составляющих его элементов, но при этом сохраняет их в изоляции:
type Flatten<T> = T extends any[] ? T[number] : T
// Извлекаем тип элемента
type Str = Flatten<string[]>
// type Str = string
// Сохраняем тип
type Num = Flatten<number>
// type Num = number
Когда Flatten
получает тип массива, он использует доступ по индексу с помощью number
для получения типа элемента string[]
. В противном случае, он просто возвращает переданный ему тип.
Предположения в условных типах
Мы использовали условные типы для применения ограничений и извлечения типов. Это является настолько распространенной операцией, что существует особая разновидность условных типов.
Условные типы предоставляют возможность делать предположения на основе сравниваемых в истинной ветке типов с помощью ключевого слова infer
. Например, мы можем сделать вывод относительно типа элемента во Flatten
вместо его получения вручную через доступ по индексу:
type Flatten<Type> = Type extends Array<infer Item> ? Item : Type
В данном случае мы использовали ключевое слово infer
для декларативного создания нового дженерика Item
вместо извлечения типа элемента T
в истинной ветке. Это избавляет нас от необходимости "копаться" и изучать структуру типов, которые нам необходимы.
Мы можем создать несколько вспомогательных синонимов типа (type aliases) с помощью infer
. Например, в простых случаях мы можем извлекать возвращаемый тип из функции:
type GetReturnType<Type> = Type extends (...args: never[]) => infer Return
? Return
: never
type Num = GetReturnType<() => number>
// type Num = number
type Str = GetReturnType<(x: string) => string>
// type Str = string
type Bools = GetReturnType<(a: boolean, b: boolean) => boolean[]>
// type Bools = boolean[]
При предположении на основе типа с помощью нескольких сигнатур вызова (такого как тип перегруженной функции), предположение выполняется на основе последней сигнатуры. Невозможно произвести разрешение перегрузки на основе списка типов аргументов.
declare function stringOrNum(x: string): number
declare function stringOrNum(x: number): string
declare function stringOrNum(x: string | number): string | number
type T1 = ReturnType<typeof stringOrNum>
// type T1 = string | number
Распределенные условные типы (distributive conditional types)
Когда условные типы применяются к дженерикам, они становятся распределенными при получении объединения (union). Рассмотрим следующий пример:
type ToArray<Type> = Type extends any ? Type[] : never
Если мы изолируем объединение в ToArray
, условный тип будет применяться к каждому члену объединения.
type ToArray<Type> = Type extends any ? Type[] : never
type StrArrOrNumArr = ToArray<string | number>
// type StrArrOrNumArr = string[] | number[]
Здесь StrOrNumArray
распределяется на:
string | number
и применяется к каждому члену объединения:
ToArray<string> | ToArray<number>
что приводит к следующему:
string[] | number[]
Обычно, такое поведение является ожидаемым. Для его изменения можно обернуть каждую сторону extends
в квадратные скобки:
type ToArrayNonDist<Type> = [Type] extends [any] ? Type[] : never
// 'StrOrNumArr' больше не является объединением
type StrOrNumArr = ToArrayNonDist<string | number>
// type StrOrNumArr = (string | number)[]
Связанные типы (mapped types)
Связанные типы основаны на синтаксисе сигнатуры доступа по индексу, который используется для определения типов свойств, которые не были определены заранее:
type OnlyBoolsAndHorses = {
[key: string]: boolean | Horse
}
const conforms: OnlyBoolsAndHorses = {
del: true,
rodney: false,
}
Связанный тип — это общий тип, использующий объединение, созданное с помощью оператора keyof
, для перебора ключей одного типа в целях создания другого:
type OptionsFlags<Type> = {
[Property in keyof Type]: boolean
}
В приведенном примере OptionsFlag
получит все свойства типа Type
и изменит их значения на boolean
.
type FeatureFlags = {
darkMode: () => void
newUserProfile: () => void
}
type FeatureOptions = OptionsFlags<FeatureFlags>
// type FeatureOptions = { darkMode: boolean, newUserProfile: boolean }
Модификаторы связывания (mapping modifiers)
Существует два модификатора, которые могут применяться в процессе связывания типов: readonly
и ?
, отвечающие за иммутабельность (неизменность) и опциональность, соответственно.
Эти модификаторы можно добавлять и удалять с помощью префиксов -
или +
. Если префикс отсутствует, предполагается +
.
// Удаляем атрибуты `readonly` из свойств типа
type CreateMutable<Type> = {
-readonly [Property in keyof Type]: Type[Property]
}
type LockedAccount = {
readonly id: string
readonly name: string
}
type UnlockedAccount = CreateMutable<LockedAccount>
// type UnlockedAccount = { id: string, name: string }
// Удаляем атрибуты `optional` из свойств типа
type Concrete<Type> = {
[Property in keyof Type]-?: Type[Property]
}
type MaybeUser = {
id: string
name?: string
age?: number
}
type User = Concrete<MaybeUser>
// type User = { id: string, name: string, age: number }
Повторное связывание ключей с помощью as
В TS
4.1 и выше, можно использовать оговорку as
для повторного связывания ключей в связанном типе:
type MappedTypeWithNewProperties<Type> = {
[Properties in keyof Type as NewKeyType]: Type[Properties]
}
Для создания новых названий свойств на основе предыдущих можно использовать продвинутые возможности, такие как типы шаблонных литералов (см. ниже):
type Getters<Type> = {
[Property in keyof Type as `get${Capitalize<string & Property>}`]: () => Type[Property]
}
interface Person {
name: string
age: number
location: string
}
type LazyPerson = Getters<Person>
// type LazyPerson = { getName: () => string, getAge: () => number, getLocation: () => string }
Ключи можно фильтровать с помощью never
в условном типе:
// Удаляем свойство `kind`
type RemoveKindField<Type> = {
[Property in keyof Type as Exclude<Property, 'kind'>]: Type[Property]
}
interface Circle {
kind: 'circle'
radius: number
}
type KindlessCircle = RemoveKindField<Circle>
// type KindlessCircle = { radius: number }
Связанные типы хорошо работают с другими возможностями по манипуляции типами, например, с условными типами. В следующем примере условный тип возвращает true
или false
в зависимости от того, содержит ли объект свойство pii
с литерально установленным true
:
type ExtractPII<Type> = {
[Property in keyof Type]: Type[Property] extends { pii: true } ? true : false
}
type DBFields = {
id: { format: 'incrementing' }
name: { type: string, pii: true }
}
type ObjectsNeedingGDPRDeletion = ExtractPII<DBFields>
// type ObjectsNeedingGDPRDeletion = { id: false, name: true }
Типы шаблонных литералов (template literal types)
Типы шаблонных литералов основаны на типах строковых литералов и имеют возможность превращаться в несколько строк через объединения.
Они имеют такой же синтаксис, что и шаблонные литералы в JS
, но используются на позициях типа. При использовании с конкретным литеральным типом, шаблонный литерал возвращает новый строковый литерал посредством объединения содержимого:
type World = 'world'
type Greeting = `hello ${World}`
// type Greeting = 'hello world'
Когда тип используется в интерполированной позиции, он является набором каждого возможного строкого литерала, который может быть представлен каждым членом объединения:
type EmailLocaleIDs = 'welcome_email' | 'email_heading'
type FooterLocaleIDs = 'footer_title' | 'footer_sendoff'
type AllLocaleIDs = `${EmailLocaleIDs | FooterLocaleIDs}_id`
/*
type AllLocaleIDs = 'welcome_email_id' | 'email_heading_id' | 'footer_title_id' | 'footer_sendoff_id'
*/
Для каждой интерполированной позиции в шаблонном литерале объединения являются множественными:
type AllLocaleIDs = `${EmailLocaleIDs | FooterLocaleIDs}_id`
type Lang = 'en' | 'ja' | 'pt'
type LocaleMessageIDs = `${Lang}_${AllLocaleIDs}`
/*
type LocaleMessageIDs = 'en_welcome_email_id' | 'en_email_heading_id' | 'en_footer_title_id' | 'en_footer_sendoff_id' | 'ja_welcome_email_id' | 'ja_email_heading_id' | 'ja_footer_title_id' | 'ja_footer_sendoff_id' | 'pt_welcome_email_id' | 'pt_email_heading_id' | 'pt_footer_title_id' | 'pt_footer_sendoff_id'
*/
Большие строковые объединения лучше создавать отдельно, но указанный способ может быть полезным в простых случаях.
Строковые объединения в типах
Мощь шаблонных строк в полной мере проявляется при определении новой строки на основе существующей внутри типа.
Например, обычной практикой в JS
является расширение объекта на основе его свойства. Создадим определение типа для функции, добавляющей поддержку для функции on
, которая позволяет регистрировать изменения значения:
const person = makeWatchedObject({
firstName: 'John',
lastName: 'Smith',
age: 30,
})
person.on('firstNameChanged', (newValue) => {
console.log(`Имя было изменено на ${newValue}!`)
})
Обратите внимание, что on
регистрирует событие firstNameChanged
, а не просто firstName
.
Шаблонные литералы предоставляют способ обработки такой операции внутри системы типов:
type PropEventSource<Type> = {
on(eventName: `${string & keyof Type}Changed`, callback: (newValue: any) => void): void
}
// Создаем "наблюдаемый объект" с методом `on`,
// позволяющим следить за изменениями значений свойств
declare function makeWatchedObject<Type>(obj: Type): Type & PropEventSource<Type>
При передаче неправильного свойства возникает ошибка:
const person = makeWatchedObject({
firstName: 'John',
lastName: 'Smith',
age: 26
})
person.on('firstNameChanged', () => {})
person.on('firstName', () => {})
// Argument of type '"firstName"' is not assignable to parameter of type '"firstNameChanged" | "lastNameChanged" | "ageChanged"'.
// Параметр типа '"firstName"' не может быть присвоен типу...
person.on('frstNameChanged', () => {})
// Argument of type '"firstNameChanged"' is not assignable to parameter of type '"firstNameChanged" | "lastNameChanged" | "ageChanged"'.
Предположения типов с помощью шаблонных литералов
Заметьте, что в последних примерах типы оригинальных значений не использовались повторно. В функции обратного вызова использовался тип any
. Типы шаблонных литералов могут предполагаться на основе заменяемых позиций.
Мы можем переписать последний пример с дженериком таким образом, что типы будут предполагаться на основе частей строки eventName
:
type PropEventSource<Type> = {
on<Key extends string & keyof Type>
// (eventName: `${Key}Changed`, callback: (newValue: Type[Key]) => void ): void
}
declare function makeWatchedObject<Type>(obj: Type): Type & PropEventSource<Type>
const person = makeWatchedObject({
firstName: 'Jane',
lastName: 'Air',
age: 26
})
person.on('firstNameChanged', newName => {
// (parameter) newName: string
console.log(`Новое имя - ${newName.toUpperCase()}`)
})
person.on('ageChanged', newAge => {
// (parameter) newAge: number
if (newAge < 0) {
console.warn('Предупреждение! Отрицательный возраст')
}
})
Здесь мы реализовали on
в общем методе.
При вызове пользователя со строкой firstNameChanged
, TS
попытается предположить правильный тип для Key
. Для этого TS
будет искать совпадения Key
с "контентом", находящимся перед Changed
, и дойдет до строки firstName
. После этого метод on
сможет получить тип firstName
из оригинального объекта, чем в данном случае является string
. Точно также при вызове с ageChanged
, TS
обнаружит тип для свойства age
, которым является number
.
Внутренние типы манипуляций со строками (intrisic string manipulation types)
TS
предоставляет несколько типов, которые могут использоваться при работе со строками. Эти типы являются встроенными и находятся в файлах .d.ts
, создаваемых TS
.
Uppercase<StringType>
— переводит каждый символ строки в верхний регистр
type Greeting = 'Hello, world'
type ShoutyGreeting = Uppercase<Greeting>
// type ShoutyGreeting = 'HELLO, WORLD'
type ASCIICacheKey<Str extends string> = `ID-${Uppercase<Str>}`
type MainID = ASCIICacheKey<'my_app'>
// type MainID = 'ID-MY_APP'
Lowercase<StringType>
— переводит каждый символ в строке в нижний регистр
type Greeting = 'Hello, world'
type QuietGreeting = Lowercase<Greeting>
// type QuietGreeting = 'hello, world'
type ASCIICacheKey<Str extends string> = `id-${Lowercase<Str>}`
type MainID = ASCIICacheKey<'MY_APP'>
// type MainID = 'id-my_app'
Capitilize<StringType>
— переводит первый символ строки в верхний регистр
type LowercaseGreeting = 'hello, world'
type Greeting = Capitalize<LowercaseGreeting>
// type Greeting = 'Hello, world'
Uncapitilize<StringType>
— переводит первый символ строки в нижний регистр
type UppercaseGreeting = 'HELLO WORLD'
type UncomfortableGreeting = Uncapitalize<UppercaseGreeting>
// type UncomfortableGreeting = 'hELLO WORLD'
Вот как эти типы реализованы:
function applyStringMapping(symbol: Symbol, str: string) {
switch (intrinsicTypeKinds.get(symbol.escapedName as string)) {
case IntrinsicTypeKind.Uppercase: return str.toUpperCase()
case IntrinsicTypeKind.Lowercase: return str.toLowerCase()
case IntrinsicTypeKind.Capitalize: return str.charAt(0).toUpperCase() + str.slice(1)
case IntrinsicTypeKind.Uncapitalize: return str.charAt(0).toLowerCase() + str.slice(1)
}
return str
}
Облачные серверы от Маклауд быстрые и безопасные.
Зарегистрируйтесь по ссылке выше или кликнув на баннер и получите 10% скидку на первый месяц аренды сервера любой конфигурации!