Русский
Русский
English
Статистика
Реклама

Typescript книга по typescript

Перевод Карманная книга по TypeScript. Часть 4. Подробнее о функциях

08.06.2021 10:08:20 | Автор: admin

image


Я продолжаю серию публикаций адаптированного и дополненного перевода "Карманной книги по TypeScript".

Другие части:



Функции это основные строительные блоки любого приложения, будь то функции, импортируемые из другого модуля, или методы класса. В TS существует несколько способов описания того, как фукнции вызываются.


Тип функции в форме выражения (function type expressions)


Простейшим способом описания типа функции является выражение. Такие типы похожи на стрелочные функции:


function greeter(fn: (a: string) => void) { fn('Hello, World')}function printToConsole(s: string) { console.log(s)}greeter(printToConsole)

Выражение (a: string) => void означает "функция с одним параметром a типа string, которая ничего не возвращает". Как и в случае с определением функции, если тип параметра не указан, он будет иметь значение any.


Обратите внимание: название параметра является обязательным. Тип функции (string) => void означает "функция с параметром string типа any"!


Разумеется, для типа функции можно использовать синоним:


type GreetFn = (a: string) => voidfunction greeter(fn: GreetFn) { // ...}

Сигнатуры вызова (call signatures)


В JS функции, кроме того, что являются вызываемыми (callable), могут иметь свойства. Однако, тип-выражение не позволяет определять свойства функции. Для описания вызываемой сущности (entity), обладающей некоторыми свойствами, можно использовать сигнатуру вызова (call signature) в объектном типе:


type DescFn = { description: string (someArg: number): boolean}function doSomething(fn: DescFn) { console.log(`Значением, возвращаемым ${fn.description} является ${fn(6)}`)}

Обратите внимание: данный синтаксис немного отличается от типа-выражения функции между параметрами и возвращаемым значением используется : вместо =>.


Сигнатуры конструктора (construct signatures)


Как известно, функции могут вызываться с ключевым словом new. TS считает такие функции конструкторами, поскольку они, как правило, используются для создания объектов. Для определения типов таких функций используется сигнатура конструктора:


type SomeConstructor = { new (s: string): SomeObject}function fn(ctor: SomeConstructor) { return new ctor('Hello!')}

Некоторые объекты, такие, например, как объект Date, могут вызываться как с, так и без new. Сигнатуры вызова и конструктора можно использовать совместно:


interface CallOrConstruct { new (s: string): Date (n?: number): number}

Общие функции или функции-дженерики (generic functions)


Часто тип данных, возвращаемых функцией, зависит от типа передаваемого функции аргумента или же два типа возвращаемых функцией значений зависят друг от друга. Рассмотрим функцию, возвращающую первый элемент массива:


function firstElement(arr: any[]) { return arr[0]}

Функция делают свою работу, но, к сожалению, типом возвращаемого значения является any. Было бы лучше, если бы функция возвращала тип элемента массива.


В TS общие типы или дженерики (generics) используются для описания связи между двумя значениями. Это делается с помощью определения параметра Type в сигнатуре функции:


function firstElement<Type>(arr: Type[]): Type { return arr[0]}

Добавив параметр Type и использовав его в двух местах, мы создали связь между входящими данными функции (массивом) и ее выходными данными (возвращаемым значением). Теперь при вызове функции возвращается более конкретный тип:


// `s` имеет тип `string`const s = firstElement(['a', 'b', 'c'])// `n` имеет тип `number`const n = firstElement([1, 2, 3])

Предположение типа (inference)


Мы можем использовать несколько параметров типа. Например, самописная версия функции map может выглядеть так:


function map<Input, Output>(arr: Input[], func: (arg: Input) => Output): Output[] { return arr.map(func)}// Типом `n` является `string`,// а типом `parsed` - `number[]`const parsed = map(['1', '2', '3'], (n) => parseInt(n))

Обратите внимание, что в приведенном примере TS может сделать вывод относительно типа Input на основе переданного string[], а относительно типа Output на основе возвращаемого number.


Ограничения (constraints)


Ограничение, как следует из названия, используется для ограничения типов, принимаемых параметром типа.


Реализуем функцию, возвращающую самое длинное из двух значений. Для этого нам потребуется свойство length, которое будет числом. Мы ограничим параметр типа типом number с помощью ключевого слова extends:


function longest<Type extends { length: number }>(a: Type, b: Type) { if (a.length >= b.length) {   return a } else {   return b }}// Типом `longerArr` является `number[]`const longerArr = longest([1, 2], [1, 2, 3])// Типом `longerStr` является `string`const longerStr = longest('alice', 'bob')// Ошибка! У чисел нет свойства `length`const notOK = longest(10, 100)// Argument of type 'number' is not assignable to parameter of type '{ length: number }'.// Аргумент типа 'number' не может быть присвоен параметру типа '{ length: number; }'

Мы позволяем TS предполагать тип значения, возвращаемого из функции longest.


Поскольку мы свели Type к { length: number }, то получили доступ к свойству length параметров a и b. Без ограничения типа у нас бы не было такого доступа, потому что значения этих свойств могли бы иметь другой тип без длины.


Типы longerArr и longerStr были выведены на основе аргументов. Запомните, дженерики определяют связь между двумя и более значениями одного типа!


Наконец, как мы и ожидали, вызов longest(10, 100) отклоняется, поскольку тип number не имеет свойства length.


Работа с ограниченными значениями


Вот пример распространенной ошибки, возникающей при работе с ограничениями дженериков:


function minLength<Type extends { length: number }>( obj: Type, min: number): Type { if (obj.length >= min) {   return obj } else {   return { length: min } }}// Type '{ length: number; }' is not assignable to type 'Type'. '{ length: number; }' is assignable to the constraint of type 'Type', but 'Type' could be instantiated with a different subtype of constraint '{ length: number; }'.// Тип '{ length: number; }' не может быть присвоен типу 'Type'. '{ length: number; }' может присваиваться ограничению типа 'Type', но 'Type' может быть инстанцирован с другим подтипом ограничения '{ length: number; }'

На первый взгляд может показаться, что все в порядке Type сведен к { length: number }, и функция возвращает либо Type, либо значение, совпадающее с ограничением. Проблема состоит в том, что функция может вернуть объект, идентичный тому, который ей передается, а не просто объект, совпадающий с ограничением. Если бы во время компиляции не возникло ошибки, мы могли бы написать что-то вроде этого:


// `arr` получает значение `{ length: 6 }`const arr = minLength([1, 2, 3], 6)// и ломает приложение, поскольку массивы// имеют метод `slice`, но не возвращаемый объект!console.log(arr.slice(0))

Определение типа аргументов


Обычно, TS делает правильные выводы относительно типов аргументов в вызове дженерика, но так бывает не всегда. Допустим, мы реализовали такую функцию для объединения двух массивов:


function combine<Type>(arr1: Type[], arr2: Type[]): Type[] { return arr1.concat(arr2)}

При обычном вызове данной функции с несовпадающими по типу массивами возникает ошибка:


const arr = combine([1, 2, 3], ['привет'])// Type 'string' is not assignable to type 'number'.

Однако, мы можем вручную определить Type, и тогда все будет в порядке:


const arr = combine<string | number>([1, 2, 3], ['привет'])

Руководство по написанию хороших функций-дженериков


Используйте параметры типа без ограничений


Рассмотрим две похожие функции:


function firstElement1<Type>(arr: Type[]) { return arr[0]}function firstElement2<Type extends any[]>(arr: Type) { return arr[0]}// a: number (хорошо)const a = fisrtElement1([1, 2, 3])// b: any (плохо)const b = fisrtElement2([1, 2, 3])

Предполагаемым типом значения, возвращаемого функцией firstElement1 является Type, а значения, возвращаемого функцией firstElement2 any. Это объясняется тем, что TS разрешает (resolve) выражение arr[0] с помощью ограничения типа вместо того, чтобы ждать разрешения элемента после вызова функции.


Правило: по-возможности, используйте параметры типа без ограничений.


Используйте минимальное количество параметров типа


Вот еще одна парочка похожих функций:


function filter1<Type>(arr: Type[], func: (arg: Type) => boolean): Type[] { return arr.filter(func)}function filter2<Type, Func extends (arg: Type) => boolean>( arr: Type[], func: Func): Type[] { return arr.filter(func)}

Во втором случае мы создаем параметр типа Func, который не связывает значения. Это означает, что при вызове функции придется определять дополнительный аргумент типа без веских на то причин. Это не есть хорошо.


Правило: всегда используйте минимальное количество параметров типа.


Параметры типа должны указываться дважды


Иногда мы забываем, что функция не обязательно должна быть дженериком:


function greet<Str extends string>(s: Str) { console.log(`Привет, ${s}!`)}greet('народ')

Вот упрощенная версия данной функции:


function greet(s: string) { console.log(`Привет, ${s}!`)}

Запомните, параметры типа предназначены для связывания типов нескольких значений.


Правило: если параметр типа появляется в сигнатуре функции только один раз, то, скорее всего, он вам не нужен.


Опциональные параметры (optional parameters)


Функции в JS могут принимать произвольное количество аргументов. Например, метод toFixed принимает опциональное количество цифр после запятой:


function fn(n: number) { console.log(n.toFixed()) // 0 аргументов console.log(n.toFixed(3)) // 1 аргумент}

Мы можем смоделировать это в TS, пометив параметр как опциональный с помощью ?:


function f(x?: number) { // ...}f() // OKf(10) // OK

Несмотря на то, что тип параметра определен как number, параметр x на самом деле имеет тип number | undefined, поскольку неопределенные параметры в JS получают значение undefined.


Мы также можем указать "дефолтный" параметр (параметр по умолчанию):


function f(x = 10) { // ...}

Теперь в теле функции f параметр x будет иметь тип number, поскольку любой аргумент со значением undefined будет заменен на 10. Обратите внимание: явная передача undefined означает "отсутствующий" аргумент.


declare function f(x?: number): void// OKf()f(10)f(undefined)

Опциональные параметры в функциях обратного вызова


При написании функций, вызывающих "колбеки", легко допустить такую ошибку:


function myForEach(arr: any[], callback: (arg: any, index?: number) => void) { for (let i = 0; i < arr.length; i++) {   callback(arr[i], i) }}

Указав index?, мы хотим, чтобы оба этих вызова были легальными:


myForEach([1, 2, 3], (a) => console.log(a))myForEach([1, 2, 3], (a, i) => console.log(a, i))

В действительности, это означает, что колбек может быть вызван с одним аргументом. Другими словами, определение функции говорит, что ее реализация может выглядеть так:


function myForEach(arr: any[], callback: (arg: any, index?: number) => void) { for (let i = 0; i < arr.length; i++) {   callback(arr[i]) }}

Поэтому попытка вызова такой функции приводит к ошибке:


myForEach([1, 2, 3], (a, i) => { console.log(i.toFixed()) // Object is possibly 'undefined'. // Возможным значением объекта является 'undefined'})

В JS при вызове функции с большим (ударение на первый слог) количеством аргументов, чем указано в определении фукнции, дополнительные параметры просто игнорируются. TS ведет себя аналогичным образом. Функции с меньшим количеством параметров (одного типа) могут заменять функции с большим количеством параметров.


Правило: при написании типа функции для колбека, не указывайте опциональные параметры до тех пор, пока не будете вызывать функцию без передачи этих параметров.


Перегрузка функции (function overload)


Некоторые функции могут вызываться с разным количеством аргументов. Например, мы можем написать функцию, возвращающую Date, которая принимает время в мс (timestamp, один аргумент) или день/месяц/год (три аргумента).


В TS такую функцию можно реализовать с помощью сигнатур перегрузки (overload signatures). Для этого перед телом функции указывается несколько ее сигнатур:


function makeDate(timestamp: number): Datefunction makeDate(d: number, m: number, y: number): Datefunction makeDate(dOrTimestamp: number, m?: number, y?: number): Date { if (m !== undefined && y !== undefined) {   return new Date(y, m, dOrTimestamp) } else {   return new Date(dOrTimestamp) }}const d1 = makeDate(12345678)const d2 = makeDate(5, 5, 5)const d3 = makeDate(1, 3)// No overload expects 2 arguments, but overloads do exist that expect either 1 or 3 arguments.// Нет перегрузки, принимающей 2 аргумента, но существуют перегрузки, ожидающие получения 1 или 3 аргумента

В приведенном примере мы реализовали две перегрузки: одну, принимающую один аргумент, и вторую, принимающую три аргумента. Первые две сигнатуры называются сигнатурами перегрузки.


Затем мы реализовали функцию с совместимой сигнатурой (compatible signature). Функции имеют сигнатуру реализации (implementation signature), но эта сигнатура не может вызываться напрямую. Несмотря на то, что мы написали функцию с двумя опциональными параметрами после обязательного, она не может вызываться с двумя параметрами!


Сигнатуры перегрузки и сигнатура реализации


Предположим, что у нас имеется такой код:


function fn(x: string): voidfunction fn() { // ...}// Мы ожидаем, что функция может вызываться без аргументовfn()// Expected 1 arguments, but got 0.// Ожидалось получение 1 аргумента, а получено 0

Почему в данном случае возникает ошибка? Дело в том, что сигнатура реализации не видна снаружи (за пределами тела функции). Поэтому при написании перегруженной функции всегда нужно указывать две или более сигнатуры перегрузки перед сигнатурой реализации.


Кроме того, сигнатура реализации должна быть совместима с сигнатурами перегрузки. Например, при вызове следующих функций возникают ошибки, поскольку сигнатура реализации не совпадает с сигнатурами перегрузки:


function fn(x: boolean): void// Неправильный тип аргументаfunction fn(x: string): void// This overload signature is not compatible with its implementation signature.// Данная сигнатура перегрузки не совместима с сигнатурой ее реализацииfunction(x: boolean) {}

function fn(x: string): string// Неправильный тип возвращаемого значенияfunction(x: number): boolean// This overload signature is not compatible with its implementation signature.function fn(x: string | number) { return 'упс'}

Правила реализации хороших перегрузок функции


Рассмотрим функцию, возвращающую длину строки или массива:


function len(s: string): numberfunction len(arr: any[]): numberfunction len(x: any) { return x.length}

На первый взгляд кажется, что все в порядке. Мы можем вызывать функцию со строками или массивами. Однако, мы не можем вызывать ее со значением, которое может быть либо строкой, либо массивом, поскольку TS ассоциирует вызов функции с одной из ее перегрузок:


len('') // OKlen([0]) // OKlen(Math.random() > 0.5 ? 'привет' : [0])/*No overload matches this call. Overload 1 of 2, '(s: string): number', gave the following error.   Argument of type 'number[] | "привет"' is not assignable to parameter of type 'string'.     Type 'number[]' is not assignable to type 'string'. Overload 2 of 2, '(arr: any[]): number', gave the following error.   Argument of type 'number[] | "привет"' is not assignable to parameter of type 'any[]'.     Type 'string' is not assignable to type 'any[]'.*//*Ни одна из перегрузок не совпадает с вызовом. Перегрузка 1 из 2, '(s: string): number', возвращает следующую ошибку.   Аргумент типа 'number[] | "привет"' не может быть присвоен параметру типа 'string'.     Тип 'number[]' не может быть присвоен типу 'string'. Перегрузка 2 из 2, '(arr: any[]): number', возвращает следующую ошибку.   Аргумент типа 'number[] | "привет"' не может быть присвоен типу 'any[]'.     Тип 'string' не может быть присвоен типу 'any[]'.*/

Поскольку обе перегрузки имеют одинаковое количество аргументов и один и тот же тип возвращаемого значения, мы можем реализовать такую "неперегруженную" версию данной функции:


function len(x: any[] | string) { return x.length}

Так намного лучше! Теперь мы можем вызывать функцию с любым значением и, кроме того, нам не нужно предварительно определять правильную сигнатуру реализации функцию.


Правило: по-возможности используйте объединения вместо перегрузок функции.


Определение this в функциях


Рассмотрим пример:


const user = { id: 123, admin: false, becomeAdmin: function() {   this.admin = true }}

TS "понимает", что значением this функции user.becomeAdmin является внешний объект user. В большинстве случаев этого достаточно, но порой нам требуется больше контроля над тем, что представляет собой this. Спецификация JS определяет, что мы не можем использовать this в качестве названия параметра. TS использует это синтаксическое пространство (syntax space), позволяя определять тип this в теле функции:


const db = getDB()const admins = db.filterUsers(function() { return this.admin})

Обратите внимание: в данном случае мы не можем использовать стрелочную функцию.


const db = getDB()const admins = db.filterUsers(() => this.admin)// The containing arrow function captures the global value of 'this'. Element implicitly has an 'any' type because type 'typeof globalThis' has no index signature.// Стрелочная функция перехватывает глобальное значение 'this'. Неявным типом элемента является 'any', поскольку тип 'typeof globalThis' не имеет сигнатуры индекса

Другие типы, о которых следует знать


void


void представляет значение, возвращаемое функцией, которая ничего не возвращает. Если в теле функции отсутствует оператор return или после этого оператора не указано возвращаемого значения, предполагаемым типом возвращаемого такой функцией значения будет void:


// Предполагаемым типом является `void`function noop() { return}

В JS функция, которая ничего не возвращает, "неявно" возвращает undefined. Однако, в TS void и undefined это разные вещи.


Обратите внимание: void это не тоже самое, что undefined.


object


Специальный тип object представляет значение, которое не является примитивом (string, number, boolean, symbol, null, undefined). object отличается от типа пустого объекта ({}), а также от глобального типа Object. Скорее всего, вам никогда не потребуется использовать Object.


Правило: object это не Object. Всегда используйте object!


Обратите внимание: в JS функции это объекты: они имеют свойства, Object.prototype в цепочке прототипов, являются instanceof Object, мы можем вызывать на них Object.keys и т.д. По этой причине в TS типом функций является object.


unknown


Тип unknown представляет любое значение. Он похож на тип any, но является более безопасным, поскольку не позволяет ничего делать с неизвестным значением:


function f1(a: any) { a.b() // OK}function f2(a: unknown) { a.b() // Object is of type 'unknown'. // Типом объекта является 'unknown'}

Это бывает полезным для описания типа функции, поскольку таким способом мы можем описать функцию, принимающую любое значение без использования типа any в теле функции. Другими словами, мы можем описать функцию, возвращающую значение неизвестного типа:


function safeParse(s: string): unknown { return JSON.parse(s)}const obj = safeParse(someRandomString)

never


Некоторые функции никогда не возвращают значений:


function fail(msg: string): never { throw new Error(msg)}

Тип never представляет значение, которого не существует. Чаще всего, это означает, что функция выбрасывает исключение или останавливает выполнение программы.


never также появляется, когда TS определяет, что в объединении больше ничего не осталось:


function fn(x: string | number) { if (typeof x === 'string') {   // ... } else if (typeof x === 'number') {   // ... } else {   x // типом `x` является `never`! }}

Function


Глобальный тип Function описывает такие свойства как bind, call, apply и другие, характерные для функций в JS. Он также имеет специальное свойство, позволяющее вызывать значения типа Function такие вызовы возвращают any:


function doSomething(f: Function) { f(1, 2, 3)}

Такой вызов функции называется нетипизированным и его лучше избегать из-за небезопасного возвращаемого типа any.


Если имеется необходимость принимать произвольную функцию без ее последующего вызова, лучше предпочесть более безопасный тип () => void.


Оставшиеся параметры и аргументы


Оставшиеся параметры (rest parameters)


Кроме использования опциональных параметров или перегрузок для создания функций, принимающих разное или фиксированное количество аргументов, мы можем определять функции, принимающие произвольное количество аргументов с помощью синтаксиса оставшихся параметров.


Оставшиеся параметры указываются после других параметров с помощью ...:


function multiply(n: number, ...m: number[]) { return m.map((x) => n * x)}// `a` получает значение [10, 20, 30, 40]const a = multiply(10, 1, 2, 3, 4)

В TS неявным типом таких параметров является any[], а не any. Любая аннотация типа для них должна иметь вид Array<T> или T[], или являться кортежем.


Оставшиеся аргументы (rest arguments)


Синтаксис распространения (синонимы: расширение, распаковка) (spread syntax) позволяет передавать произвольное количество элементов массива. Например, метод массива push принимает любое количество аргументов:


const arr1 = [1, 2, 3]const arr2 = [4, 5, 6]arr1.push(...arr2)

Обратите внимание: TS не считает массивы иммутабельными. Это может привести к неожиданному поведению:


// Предполагаемым типом `args` является `number[]` - массив с 0 или более чисел// а не конкретно с 2 числамиconst args = [8, 5]const angle = Math.atan2(...args)// Expected 2 arguments, but got 0 or more.// Ожидалось получение 2 аргументов, а получено 0 или более

Самым простым решением данной проблемы является использование const:


// Предполагаемым типом является кортеж, состоящий из 2 элементовconst args = [8, 5] as const// OKconst angle = Math.atan2(...args)

Деструктуризация параметров (parameter destructuring)


Деструктуризация параметров используется для распаковки объекта, переданного в качестве аргумента, в одну или более локальную переменную в теле функции. В JS это выглядит так:


function sum({ a, b, c }) { console.log(a + b + c)}sum({ a: 10, b: 3, c: 9 })

Аннотация типа для объекта указывается после деструктуризации:


function sum({ a, b, c }: { a: number, b: number, c: number }) { console.log(a + b + c)}

Для краткости можно использовать именованный тип:


type ABC = { a: number, b: number, c: number }function sum({ a, b, c }: ABC) { console.log(a + b + c)}

Возможность присвоения функций переменным


Использование void в качестве типа возвращаемого функцией значения может приводить к необычному, но вполне ожидаемому поведению.


Контекстуальная типизация (contextual typing), основанная на void, не запрещает функции что-либо возвращать. Другими словами, функция, типом возвращаемого значения которой является void type vf = () => void, может возвращать любое значение, но это значение будет игнорироваться.


Все приведенные ниже реализации типа () => void являются валидными:


type voidFn = () => voidconst f1: voidFn = () => { return true}const f2: voidFn = () => trueconst f3: voidFn = function() { return true}

Когда возвращаемое любой из этих функций значение присваивается переменной, она будет хранить тип void:


const v1 = f1()const v2 = f2()const v3 = f3()

Поэтому следующий код является валидным, несмотря на то, что Array.prototype.push возвращает число, а Array.prototype.forEach ожидает получить функцию с типом возвращаемого значения void:


const src = [1, 2, 3]const dst = [0]src.forEach((el) => dist.push(el))

Существует один специальный случай, о котором следует помнить: когда литеральное определение функции имеет тип возвращаемого значения void, функция не должна ничего возвращать:


function f2(): void { // Ошибка return true}const f3 = function(): void { // Ошибка return true}



Облачные серверы от Маклауд быстрые и безопасные.


Зарегистрируйтесь по ссылке выше или кликнув на баннер и получите 10% скидку на первый месяц аренды сервера любой конфигурации!


Подробнее..

Перевод Карманная книга по TypeScript. Часть 5. Объектные типы

10.06.2021 12:09:47 | Автор: admin

image


Доброго времени суток, друзья! Мы продолжаем серию публикаций адаптированного и дополненного перевода "Карманной книги по TypeScript".

Другие части:



В JS обычным способом группировки и передачи данных являются объекты. В TS они представлены объектными типами (object types).


Как мы видели ранее, они могут быть анонимными:


function greet(person: { name: string, age: number }) { return `Привет, ${person.name}!`}

или именоваться с помощью интерфейсов (interfaces):


interface Person { name: string age: number}function greet(person: Person) { return `Привет, ${person.name}!`}

или синонимов типа (type aliases):


type Person { name: string age: number}function greet(person: Person) { return `Привет, ${person.name}!`}

Во всех приведенных примерах наша функция принимает объект, который содержит свойство name (значение которого должно быть типа string) и age (значение которого должно быть типа number).


Модификаторы свойств (property modifiers)


Каждое свойство в объектном типе может определять несколько вещей: сам тип, то, является ли свойство опциональным, и может ли оно изменяться.


Опциональные свойства (optional properties)


Свойства могут быть помечены как опциональные (необязательные) путем добавления вопросительного знака (?) после их названий:


interface PaintOptions { shape: Shape xPos?: number yPos?: number}function paintShape(opts: PaintOptions) { // ...}const shape = getShape()paintShape({ shape })paintShape({ shape, xPos: 100 })paintShape({ shape, yPos: 100 })paintShape({ shape, xPos: 100, yPos: 100 })

Все вызовы функции в приведенном примере являются валидными. Опциональность означает, что если свойство установлено, оно должно иметь указанный тип.


Мы можем получать значения таких свойств. Однако, при включенной настройке strictNullChecks, мы будем получать сообщения о том, что потенциальными значениями опциональных свойств является undefined:


function paintShape(opts: PaintOptions) { let xPos = opts.xPos               // (property) PaintOptions.xPos?: number | undefined let yPos = opts.yPos               // (property) PaintOptions.yPos?: number | undefined // ...}

В JS при доступе к несуществующему свойству возвращается undefined. Добавим обработку этого значения:


function paintShape(opts: PaintOptions) { let xPos = opts.xPos === undefined ? 0 : opts.xPos   // let xPos: number let yPos = opts.yPos === undefined ? 0 : opts.yPos   // let yPos: number // ...}

Теперь все в порядке. Но для определения "дефолтных" значений (значений по умолчанию) параметров в JS существует специальный синтаксис:


function paintShape({ shape, xPos = 0, yPos = 0 }: PaintOptions) { console.log('x coordinate at', xPos)                               // var xPos: number console.log('y coordinate at', yPos)                               // var yPos: number // ...}

В данном случае мы деструктурировали параметр painShape и указали значения по умолчанию для xPos и yPos. Теперь они присутствуют в теле функции painShape, но являются опциональными при ее вызове.


Обратите внимание: в настоящее время не существует способа поместить аннотацию типа в деструктуризацию, поскольку такой синтаксис будет интерпретирован JS иначе:


function draw({ shape: Shape, xPos: number = 100 /*...*/ }) { render(shape) // Cannot find name 'shape'. Did you mean 'Shape'? // Невозможно найти 'shape'. Возможно, вы имели ввиду 'Shape' render(xPos) // Cannot find name 'xPos'. // Невозможно найти 'xPos'}

shape: Shape означает "возьми значение свойства shape и присвой его локальной переменной Shape". Аналогично xPos: number создает переменную number, значение которой основано на параметре xPos.


Свойства, доступные только для чтения (readonly properties)


Свойства могут быть помечены как доступные только для чтения с помощью ключевого слова readonly. Такие свойства не могут перезаписываться в процессе проверки типов:


interface SomeType { readonly prop: string}function doSomething(obj: SomeType) { // Мы может читать (извлекать значения) из 'obj.prop'. console.log(`prop has the value '${obj.prop}'.`) // Но не можем изменять значение данного свойства obj.prop = 'hello' // Cannot assign to 'prop' because it is a read-only property. // Невозможно присвоить значение 'prop', поскольку оно является доступным только для чтения}

Использование модификатора readonly не делает саму переменную иммутабельной (неизменяемой), это лишь запрещает присваивать ей другие значения:


interface Home { readonly resident: { name: string, age: number }}function visitForBirthday(home: Home) { // Мы можем читать и обновлять свойства 'home.resident'. console.log(`С Днем рождения, ${home.resident.name}!`) home.resident.age++}function evict(home: Home) { // Но мы не можем изменять значение свойства 'resident' home.resident = { // Cannot assign to 'resident' because it is a read-only property.   name: 'Victor the Evictor',   age: 42, }}

readonly сообщает TS, как должны использоваться объекты. При определении совместимости двух типов TS не проверяет, являются ли какие-либо свойства доступными только для чтения. Поэтому такие свойства можно изменять с помощью синонимов:


interface Person { name: string age: number}interface ReadonlyPerson { readonly name: string readonly age: number}let writablePerson: Person = { name: 'John Smith', age: 42}// работаетlet readonlyPerson: ReadonlyPerson = writablePersonconsole.log(readonlyPerson.age) // 42writablePerson.age++console.log(readonlyPerson.age) // 43

Сигнатуры индекса (index signatures)


Иногда мы не знаем названий всех свойств типа, но знаем форму значений.


В таких случаях мы можем использовать индексы для описания типов возможных значений, например:


interface StringArray { [index: number]: string}const myArray: StringArray = getStringArray()const secondItem = myArray[1]   // const secondItem: string

В приведенном примере у нас имеется интерфейс StringArray, содержащий сигнатуру индекса. Данная сигнатура указывает на то, что при индексации StringArray с помощью number возвращается string.


Сигнатура индекса типа свойства должна быть строкой или числом.


Несмотря на поддержку обоих типов индексаторов (indexers), тип, возвращаемый из числового индексатора, должен быть подтипом типа, возвращаемого строковым индексатором. Это объясняется тем, что при индексации с помощью number, JS преобразует его в string перед индексацией объекта. Это означает, что индексация с помощью 100 (number) эквивалента индексации с помощью "100" (string), поэтому они должны быть согласованными между собой.


interface Animal { name: string}interface Dog extends Animal { breed: string}// Ошибка: индексация с помощью числовой строки может привести к созданию другого типа Animal!interface NotOkay { [x: number]: Animal // Numeric index type 'Animal' is not assignable to string index type 'Dog'. // Числовой индекс типа 'Animal' не может быть присвоен строковому индексу типа 'Dog' [x: string]: Dog}

В то время, как сигнатуры строкового индекса являются хорошим способом для описания паттерна "словарь", они предопределяют совпадение всех свойств их возвращаемым типам. Это объясняется тем, что строковый индекс определяет возможность доступа к obj.property с помощью obj['property']. В следующем примере тип name не совпадает с типом строкового индекса, поэтому во время проверки возникает ошибка:


interface NumberDictionary { [index: string]: number length: number // ok name: string // Property 'name' of type 'string' is not assignable to string index type 'number'.}

Тем не менее, свойства с разными типами являются валидными в случае, когда сигнатура индекса это объединение типов (union):


interface NumberOrStringDictionary { [index: string]: number | string length: number // ok, `length` - это число name: string // ok, `name` - это строка}

Сигнатуры индекса можно сделать доступными только для чтения для предотвращения их перезаписи:


interface ReadonlyStringArray { readonly [index: number]: string}let myArray: ReadonlyStringArray = getReadOnlyStringArray()myArray[2] = 'John'// Index signature in type 'ReadonlyStringArray' only permits reading.// Сигнатура индекса в типе 'ReadonlyStringArray' допускает только чтение

Расширение типов (extending types)


Что если мы хотим определить тип, который является более конкретной версией другого типа? Например, у нас может быть тип BasicAddress, описывающий поля, необходимые для отправки писем и посылок в США:


interface BasicAddress { name?: string street: string city: string country: string postalCode: string}

В некоторых случаях этого будет достаточно, однако адреса часто имеют литералы. Для таких случаев мы можем определить AddressWithUnit:


interface AddressWithUnit { name?: string unit: string street: string city: string country: string postalCode: string}

Неужели не существует более простого способа добавления дополнительных полей? На самом деле, мы можем просто расширить BasicAddress, добавив к нему новые поля, которые являются уникальными для AddressWithUnit:


interface BasicAddress { name?: string street: string city: string country: string postalCode: string}interface AddressWithUnit extends BasicAddress { unit: string}

Ключевое слово extends позволяет копировать членов именованных типов в другие типы. Оно также указывает на связь между типами.


Интерфейсы также могут расширяться с помощью нескольких типов одновременно:


interface Colorful { color: string}interface Circle { radius: number}interface ColorfulCircle extends Colorful, Circle {}const cc: ColorfulCircle = { color: 'red', radius: 42}

Пересечение типов (intersection types)


interface позволяет создавать новые типы на основе других посредством их расширения. TS также предоставляет другую конструкцию, которая называется пересечением типов или пересекающимися типами и позволяет комбинировать существующие объектные типы. Пересечение типов определяется с помощью оператора &:


interface Colorful { color: string}interface Circle { radius: number}type ColorfulCircle = Colorful & Circle

Пересечение типов Colorful и Circle приводит к возникновению типа, включающего все поля Colorful и Circle:


function draw(circle: Colorful & Circle) { console.log(`Цвет круга: ${circle.color}`) console.log(`Радиус круга: ${circle.radius}`)}// OKdraw({ color: 'blue', radius: 42 })// опечаткаdraw({ color: 'red', raidus: 42 })/*Argument of type '{ color: string, raidus: number }' is not assignable to parameter of type 'Colorful & Circle'. Object literal may only specify known properties, but 'raidus' does not exist in type 'Colorful & Circle'. Did you mean to write 'radius'?*//*Аргумент типа '{ color: string, raidus: number }' не может быть присвоен параметру с типом 'Colorful & Circle'. С помощью литерала объекта могут определяться только известные свойства, а свойства с названием 'raidus' не существует в типе 'Colorful & Circle'. Возможно, вы имели ввиду 'radius'*/

Интерфейс или пересечение типов?


И интерфейсы, и пересечения типов используются для создания новых типов на основе существующих за счет комбинирования последних. Основное отличие между ними заключается в том, как обрабатываются возникающие конфликты.


Общие объектные типы (generic object types)


Предположим, что у нас имеется тип Box, который может содержать любое значение:


interface Box { contents: any}

Этот код работает, но тип any является небезопасным с точки зрения системы типов. Вместо него мы могли бы использовать unknown, но это будет означать необходимость выполнения предварительных проверок и подверженных ошибкам утверждений типов (type assertions).


interface Box { contents: unknown}let x: Box { contents: 'привет, народ'}// мы можем проверить `x.contents`if (typeof x.contents === 'string') { console.log(x.contents.toLowerCase())}// или можем использовать утверждение типаconsole.log((x.contents as string).toLowerCase())

Более безопасным способом будет определение различных типов Box для каждого типа contents:


interface NumberBox { contents: number}interface StringBox { contents: string}interface BooleanBox { contents: boolean}

Однако, это обуславливает необходимость создания различных функций или перегрузок функции (function overloads) для работы с такими типами:


function setContents(box: StringBox, newContents: string): voidfunction setContents(box: NumberBox, newContents: number): voidfunction setContents(box: BooleanBox, newContents: boolean): voidfunction setContents(box: { contents: any }, newContents: any) { box.contents = newContents}

Слишком много шаблонного кода. Более того, в будущем нам может потребоваться определить новый тип и перегрузку. Так не пойдет.


Для решения данной проблемы мы можем создать общий (generic) тип Box, в котором объявляется параметр типа (type parameter):


interface Box<Type> { contents: Type}

Затем, при ссылке на Box, мы должны определить аргумент типа (type argument) вместо Type:


let box: Box<string>

По сути, Box это шаблон для настоящего типа, в котором Type будет заменен на конкретный тип. Когда TS видит Box<string>, он заменяет все вхождения Type в Box<Type> на string и заканчивает свою работу чем-то вроде { contents: string }. Другими словами, Box<string> работает также, как рассмотренный ранее StringBox.


interface Box<Type> { contents: Type}interface StringBox { contents: string}let boxA: Box<string> = { contents: 'привет' }boxA.contents     // (property) Box<string>.contents: stringlet boxB: StringBox = { contents: 'народ' }boxB.contents     // (property) StringBox.contents: string

Тип Box теперь является переиспользуемым (т.е. имеется возможность использовать этот тип несколько раз без необходимости его модификации). Это означает, что когда нам потребуется коробка (Box коробка, контейнер) нового типа, нам не придется определять новый тип Box:


interface Box<Type> { contents: Type}interface Apple { // ....}// Тоже самое, что '{ contents: Apple }'.type AppleBox = Box<Apple>

Это также означает, что нам не нужны перегрузки функции. Вместо них мы можем использовать общую функцию (generic function):


function setContents<Type>(box: Box<Type>, newContents: Type) { box.contents = newContents}

Синонимы типов также могут быть общими. Вот как мы можем определить общий тип (generic type) Box:


type Box<Type> = { contents: Type}

Поскольку синонимы, в отличие от интерфейсов, могут использоваться для описания любых типов, а не только типов объектов, мы можем использовать их следующим образом:


type OrNull<Type> = Type | nulltype OneOrMany<Type> = Type | Type[]type OneOrManyOrNull<Type> = OrNull<OneOrMany<Type>>         // type OneOrManyOrNull<Type> = OneOrMany<Type> | nulltype OneOrManyOrNullStrings = OneOrManyOrNull<string>         // type OneOrManyOrNullStrings = OneOrMany<string> | null

Тип Array


Синтаксис number[] или string[] это сокращения для Array<number> и Array<string>, соответственно:


function doSomething(value: Array<string>) { // ...}let myArray: string[] = ['hello', 'world']// оба варианта являются рабочими!doSomething(myArray)doSomething(new Array('hello', 'world'))

Array сам по себе является общим типом:


interface Array<Type> { /**  *  Получает или устанавливает длину массива  */ length: number /**  * Удаляет последний элемент массива и возвращает его  */ pop(): Type | undefined /**  * Добавляет новые элементы в конец массива и возвращает новую длину массива  */ push(...items: Type[]): number // ...}

Современный JS также предоставляет другие общие структуры данных, такие как Map<K, V>, Set<T> и Promise<T>. Указанные структуры могут работать с любым набором типов.


Тип ReadonlyArray


ReadonlyArray это специальный тип, описывающий массив, который не должен изменяться.


function doStuff(values: ReadonlyArray<string>) { // Мы можем читать из `values`... const copy = values.slice() console.log(`Первым значением является ${values[0]}`) // но не можем их изменять values.push('Привет!') // Property 'push' does not exist on type 'readonly string[]'. // Свойства с названием 'push' не существует в типе 'readonly string[]'}

Когда мы создаем функцию, которая возвращает ReadonlyArray, это означает, что мы не собираемся изменять такой массив, а когда мы видим функцию, принимающую ReadonlyArray, это означает, что мы можем передавать такой функции любой массив и не беспокоиться о том, что он может измениться.


В отличие от Array, ReadonlyArray не может использоваться как конструктор:


new ReadonlyArray('red', 'green', 'blue')// 'ReadonlyArray' only refers to a type, but is being used as a value here.// 'ReadonlyArray' всего лишь указывает на тип, поэтому не может использовать в качестве значения

Однако, мы можем присваивать массиву, доступному только для чтения, обычные массивы:


const roArray: ReadonlyArray<string> = ['red', 'green', 'blue']

Для определения массива, доступного только для чтения, также существует сокращенный синтаксис, который выглядит как readonly Type[]:


function doStuff(values: readonly string[]) { // Мы можем читать из `values`... const copy = values.slice() console.log(`The first value is ${values[0]}`) // но не можем их изменять values.push('hello!') // Property 'push' does not exist on type 'readonly string[]'.}

В отличие от модификатора свойств readonly, присваивание между Array и ReadonlyArray является однонаправленным (т.е. только обычный массив может быть присвоен доступному только для чтения массиву):


let x: readonly string[] = []let y: string[] = []x = yy = x// The type 'readonly string[]' is 'readonly' and cannot be assigned to the mutable type 'string[]'.// Тип 'readonly string[]' является доступным только для чтения и не может быть присвоен изменяемому типу 'string[]'

Кортеж (tuple)


Кортеж это еще одна разновидность типа Array с фиксированным количеством элементов определенных типов.


type StrNumPair = [string, number]

StrNumPair это кортеж string и number. StrNumPair описывает массив, первый элемент которого (элемент под индексом 0) имеет тип string, а второй (элемент под индексом 1) number.


function doSomething(pair: [string, number]) { const a = pair[0]     // const a: string const b = pair[1]     // const b: number // ...}doSomething(['hello', 42])

Если мы попытаемся получить элемент по индексу, превосходящему количество элементов, то получим ошибку:


function doSomething(pair: [string, number]) { // ... const c = pair[2] // Tuple type '[string, number]' of length '2' has no element at index '2'. // Кортеж '[string, number]' длиной в 2 элемента не имеет элемента под индексом '2'}

Кортежи можно деструктурировать:


function doSomething(stringHash: [string, number]) { const [inputString, hash] = stringHash console.log(inputString)               // const inputString: string console.log(hash)           // const hash: number}

Рассматриваемый кортеж является эквивалентом такой версии типа Array:


interface StringNumberPair { // Конкретные свойства length: 2 0: string 1: number // Другие поля 'Array<string | number>' slice(start?: number, end?: number): Array<string | number>}

Элементы кортежа могут быть опциональными (?). Такие элементы указываются в самом конце и влияют на тип свойства length:


type Either2dOr3d = [number, number, number?]function setCoords(coord: Either2dOr3d) { const [x, y, z] = coord           // const z: number | undefined console.log(`   Переданы координаты в ${coord.length} направлениях `)                               // (property) length: 2 | 3}

Кортежи также могут содержать оставшиеся элементы (т.е. элементы, оставшиеся не использованными, rest elements), которые должны быть массивом или кортежем:


type StringNumberBooleans = [string, number, ...boolean[]]type StringBooleansNumber = [string, ...boolean[], number]type BooleansStringNumber = [...boolean[], string, number]

...boolean[] означает любое количество элементов типа boolean.


Такие кортежи не имеют определенной длины (length) они имеют лишь набор известных элементов на конкретных позициях:


const a: StringNumberBooleans = ['hello', 1]const b: StringNumberBooleans = ['beautiful', 2, true]const c: StringNumberBooleans = ['world', 3, true, false, true, false, true]

Кортежи сами могут использоваться в качестве оставшихся параметров и аргументов. Например, такой код:


function readButtonInput(...args: [string, number, ...boolean[]]) { const [name, version, ...input] = args // ...}

является эквивалентом следующего:


function readButtonInput(name: string, version: number, ...input: boolean[]) { // ...}

Кортежи, доступные только для чтения (readonly tuple types)


Кортежи, доступные только для чтения, также определяются с помощью модификатора readonly:


function doSomething(pair: readonly [string, number]) { // ...}

Попытка перезаписи элемента такого кортежа приведет к ошибке:


function doSomething(pair: readonly [string, number]) { pair[0] = 'Привет!' // Cannot assign to '0' because it is a read-only property.}

Кортежи предназначены для определения типов иммутабельных массивов, так что хорошей практикой считается делать их доступными только для чтения. Следует отметить, что предполагаемым типом массива с утверждением const является readonly кортеж:


let point = [3, 4] as constfunction distanceFromOrigin([x, y]: [number, number]) { return Math.sqrt(x ** 2 + y ** 2)}distanceFromOrigin(point)/*Argument of type 'readonly [3, 4]' is not assignable to parameter of type '[number, number]'. The type 'readonly [3, 4]' is 'readonly' and cannot be assigned to the mutable type '[number, number]'.*/

В приведенном примере distanceFromOrigin не изменяет элементы переданного массива, но ожидает получения изменяемого кортежа. Поскольку предполагаемым типом point является readonly [3, 4], он несовместим с [number, number], поскольку такой тип не может гарантировать иммутабельности элементов point.




Облачные серверы от Маклауд быстрые и безопасные.


Зарегистрируйтесь по ссылке выше или кликнув на баннер и получите 10% скидку на первый месяц аренды сервера любой конфигурации!


Подробнее..

Перевод Карманная книга по TypeScript. Часть 6. Манипуляции с типами

15.06.2021 16:19:06 | Автор: admin

image


Мы продолжаем серию публикаций адаптированного и дополненного перевода "Карманной книги по TypeScript".

Другие части:



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


Обратите внимание на следующее:


  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: 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% скидку на первый месяц аренды сервера любой конфигурации!


Подробнее..

Перевод Карманная книга по TypeScript. Часть 8. Модули

21.06.2021 10:15:25 | Автор: admin

image


Мы продолжаем серию публикаций адаптированного и дополненного перевода "Карманной книги по TypeScript".

Другие части:



Определение модуля


В TS, как и в ECMAScript2015, любой файл, содержащий import или export верхнего уровня (глобальный), считается модулем.


Файл, не содержащий указанных ключевых слов, является глобальным скриптом.


Модули выполняются в собственной области видимости, а не в глобальной. Это означает, что переменные, функции, классы и т.д., объявленные в модуле, недоступны за пределами модуля до тех пор, пока они в явном виде не будут из него экспортированы. Кроме того, перед использованием экспортированных сущностей, их следует импортировать в соответствующий файл.


Не модули


Для начала, давайте разберемся, что TS считает модулем. Спецификация JS определяет, что любой файл без export или await верхнего уровня является скриптом, а не модулем.


Переменные и типы, объявленные в скрипте, являются глобальными (имеют глобальную область видимости), для объединения нескольких файлов на входе в один на выходе следует использовать либо настроку компилятора outFile, либо несколько элементов script в разметке (указанных в правильном порядке).


Если у нас имеется файл, который не содержит import или export, но мы хотим, чтобы этот файл считался модулем, просто добавляем в него такую строку:


export {}

Модули в TS


Существует 3 вещи, на которые следует обращать внимание при работе с модулями в TS:


  • Синтаксис: какой синтаксис я хочу использовать для импорта и экспорта сущностей?
  • Разрешение модулей: каковы отношения между названиями модулей (или их путями) и файлами на диске?
  • Результат: на что должен быть похож код модуля?

Синтаксис


Основной экспорт в файле определяется с помощью export default:


// @filename: hello.tsexport default function helloWorld() {  console.log('Привет, народ!')}

Затем данная функция импортируется следующим образом:


import hello from './hello.js'hello()

В дополнению к экспорту по умолчанию, из файла может экспортироваться несколько переменных и функций с помощью export (без default):


// @filename: maths.tsexport var pi = 3.14export let squareTwo = 1.41export const phi = 1.61export class RandomNumberGenerator {}export function absolute(num: number) {  if (num < 0) return num * -1  return num}

Указанные сущности импортируются так:


import { pi, phi, absolute } from './maths.js'console.log(pi)const absPhi = absolute(phi)  // const absPhi: number

Дополнительный синтаксис импорта


Название импортируемой сущности можно менять с помощью import { old as new }:


import { pi as  } from './maths.js'console.log()        /*          (alias) var : number          import         */

Разные способы импорта можно смешивать:


// @filename: maths.tsexport const pi = 3.14export default class RandomNumberGenerator {}// @filename: app.tsimport RNGen, { pi as  } from './maths.js'RNGen/*  (alias) class RNGen  import RNGen*/console.log()/*  (alias) const : 3.14  import */

Все экспортированные объекты при импорте можно поместить в одно пространство имен с помощью * as name:


// @filename: app.tsimport * as math from './maths.js'console.log(math.pi)const positivePhi = math.absolute(math.phi)  // const positivePhi: number

Файлы можно импортировать без указания переменных:


// @filename: app.tsimport './maths.js'console.log('3.14')

В данном случае import ничего не делает. Тем не менее, весь код из maths.ts вычисляется (оценивается), что может привести к запуску побочных эффектов, влияющих на другие объекты.


Специфичный для TS синтаксис модулей


Типы могут экспортироваться и импортироваться с помощью такого же синтаксиса, что и значения в JS:


// @filename: animal.tsexport type Cat = { breed: string, yearOfBirth: number }export interface Dog {  breeds: string[]  yearOfBirth: number}// @filename: app.tsimport { Cat, Dog } from './animal.js'type Animals = Cat | Dog

TS расширяет синтаксис import с помощью import type, что позволяет импортировать только типы.


// @filename: animal.tsexport type Cat = { breed: string, yearOfBirth: number }// 'createCatName' cannot be used as a value because it was imported using 'import type'.// 'createCatName' не может использоваться в качестве значения, поскольку импортируется с помощью 'import type'export type Dog = { breeds: string[], yearOfBirth: number }export const createCatName = () => 'fluffy'// @filename: valid.tsimport type { Cat, Dog } from './animal.js'export type Animals = Cat | Dog// @filename: app.tsimport type { createCatName } from './animal.js'const name = createCatName()

Такой импорт сообщает транспиляторам, вроде Babel, swc или esbuild, какой импорт может быть безопасно удален.


Синтаксис ES-модулей с поведением CommonJS


Синтаксис ES-модулей в TS напрямую согласуется с CommonJS и require из AMD. Импорт с помощью ES-модулей в большинстве случаев представляет собой тоже самое, что require в указанных окружениях, он позволяет обеспечить полное совпадение TS-файла с результатом CommonJS:


import fs = require('fs')const code = fs.readFileSync('hello.ts', 'utf8')

Синтаксис CommonJS


CommonJS это формат, используемый большинством npm-пакетов. Даже если вы используете только синтаксис ES-модулей, понимание того, как работает CommonJS, поможет вам в отладке приложений.


Экспорт


Идентификаторы экпортируются посредством установки свойства exports глобальной переменной module:


function absolute(num: number) {  if (num < 0) return num * -1  return num}module.exports = {  pi: 3.14,  squareTwo: 1.41,  phi: 1.61,  absolute}

Затем эти файлы импортируются с помощью инструкции require:


const maths = require('maths')maths.pi  // any

В данном случае импорт можно упростить с помощью деструктуризации:


const { squareTwo } = require('maths')squareTwo  // const squareTwo: any

Взаимодействие CommonJS с ES-модулями


Между CommonJS и ES-модулями имеется несовпадение, поскольку ES-модули поддерживают "дефолтный" экспорт только объектов, но не функций. Для преодоления данного несовпадения в TS используется флаг компиляции esModuleInterop.


Настройки, связанные с разрешением модулей


Разрешение модулей это процесс определения файла, указанного в качестве ссылки в строке из инструкции import или require.


TS предоставляет две стратегии разрешения модулей: классическую и Node. Классическая стратегия является стратегией по умолчанию (когда флаг module имеет значение, отличное от commonjs) и включается для обеспечения обратной совместимости. Стратегия Node имитирует работу Node.js в режиме CommonJS с дополнительными проверками для .ts и .d.ts.


Существует большое количество флагов, связанных с разрешением модулей: moduleResolution, baseUrl, paths, rootDirs и др.


Настройки для результатов разрешения модулей


Имеется две настройки, которые влияют на результирующий JS-код:


  • target определяет версию JS, в которую компилируется TS-код
  • module определяет, какой код используется для взаимодействия модулей между собой

То, какую цель (target) использовать, зависит от того, в какой среде будет выполняться код (какие возможности поддерживаются этой средой). Это может включать в себя поддержку старых браузеров, более низкую версию Node.js или специфические ограничения, накладываемые такими средами выполнения, как, например, Electron.


Коммуникация между модулями происходит через загрузчик модулей (module loader), определяемый в настройке module. Во время выполнения загрузчик отвечает за локализацию и установку всех зависимостей модуля перед его выполнением.


Ниже приведено несколько примеров использования синтаксиса ES-модулей с разными настройками module:


import { valueOfPi } from './constants.js'export const twoPi = valueOfPi * 2

ES2020


import { valueOfPi } from './constants.js'export const twoPi = valueOfPi * 2

CommonJS


"use strict";Object.defineProperty(exports, "__esModule", { value: true });exports.twoPi = void 0;const constants_js_1 = require("./constants.js");exports.twoPi = constants_js_1.valueOfPi * 2;

UMD


(function (factory) {  if (typeof module === "object" && typeof module.exports === "object") {    var v = factory(require, exports);    if (v !== undefined) module.exports = v;  }  else if (typeof define === "function" && define.amd) {    define(["require", "exports", "./constants.js"], factory);  }})(function (require, exports) {  "use strict";  Object.defineProperty(exports, "__esModule", { value: true });  exports.twoPi = void 0;  const constants_js_1 = require("./constants.js");  exports.twoPi = constants_js_1.valueOfPi * 2;});

Пространства имен (namespaces)


TS имеет собственный модульный формат, который называется namespaces. Данный синтаксис имеет множество полезных возможностей по созданию сложных файлов определений и по-прежнему активно используется в DefinitelyTyped. Несмотря на то, что namespaces не признаны устаревшими (deprecated), большая часть его возможностей нашла воплощение в ES-модулях, поэтому настоятельно рекомендуется использовать официальный синтаксис.




VPS серверы от Маклауд быстрые и безопасные.


Зарегистрируйтесь по ссылке выше или кликнув на баннер и получите 10% скидку на первый месяц аренды сервера любой конфигурации!


Подробнее..

Категории

Последние комментарии

  • Имя: Макс
    24.08.2022 | 11:28
    Я разраб в IT компании, работаю на арбитражную команду. Мы работаем с приламы и сайтами, при работе замечаются постоянные баны и лаги. Пацаны посоветовали сервис по анализу исходного кода,https://app Подробнее..
  • Имя: 9055410337
    20.08.2022 | 17:41
    поможем пишите в телеграм Подробнее..
  • Имя: sabbat
    17.08.2022 | 20:42
    Охренеть.. это просто шикарная статья, феноменально круто. Большое спасибо за разбор! Надеюсь как-нибудь с тобой связаться для обсуждений чего-либо) Подробнее..
  • Имя: Мария
    09.08.2022 | 14:44
    Добрый день. Если обладаете такой информацией, то подскажите, пожалуйста, где можно найти много-много материала по Yggdrasil и его уязвимостях для написания диплома? Благодарю. Подробнее..
© 2006-2024, personeltest.ru