Skip to main content

Манипуляции с типами

Система типов 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 описывают одну и ту же функцию, которая делает выбор на основе типов входных данных.

Обратите внимание
  1. Если библиотека будет выполнять такую проверку снова и снова, это будет не очень рациональным.
  2. Нам пришлось создать 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
}