Мы продолжаем серию публикаций адаптированного и дополненного
перевода
"Карманной книги по TypeScript
".
Другие части:
- Часть 1. Основы
- Часть 2. Типы на каждый день
- Часть 3. Сужение типов
- Часть 4. Подробнее о функциях
- Часть 5. Объектные типы
Система типов 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 = 0myGenericNum.add = (x, y) => x + y
В случае с данным классом мы не ограничены числами. Мы вполне можем использовать строки или сложные объекты:
const stringNumeric = new GenericNumber<string>()stringNumeric.zeroValue = ''stringNumeric.add = (x, y) => x + yconsole.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.nametagcreateInstance(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 = numbertype 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) => booleantype 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 | numbertype I2 = Person[keyof Person] // type I2 = string | number | booleantype 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 = numbertype 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): IdLabelfunction createLabel(name: string): NameLabelfunction createLabel(nameOrId: string | number): IdLabel | NameLabelfunction 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: NameLabellet b = createLabel(2.8) // let b: IdLabellet 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'] : neverinterface Email { message: string}interface Dog { bark(): void}type EmailMessageContents = MessageOf<Email> // type EmailMessageContents = stringtype 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 : nevertype Num = GetReturnType<() => number> // type Num = numbertype Str = GetReturnType<(x: string) => string> // type Str = stringtype Bools = GetReturnType<(a: boolean, b: boolean) => boolean[]> // type Bools = boolean[]
При предположении на основе типа с помощью нескольких сигнатур вызова (такого как тип перегруженной функции), предположение выполняется на основе последней сигнатуры. Невозможно произвести разрешение перегрузки на основе списка типов аргументов.
declare function stringOrNum(x: string): numberdeclare function stringOrNum(x: number): stringdeclare function stringOrNum(x: string | number): string | numbertype 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[] : nevertype 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% скидку на первый месяц аренды сервера любой конфигурации!