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

Основы

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

29.05.2021 16:20:40 | Автор: admin

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


Каждое значение в JavaScript при выполнении над ним каких-либо операций ведет себя определенным образом. Это может звучать несколько абстрактно, но, в качестве примера, попробуем выполнить некоторые операции над переменной message:


// Получаем доступ к свойству `toLowerCase`// и вызываем егоmessage.toLowerCase()// Вызываем `message`message()

На первой строке мы получаем доступ к свойству toLowerCase и вызываем его. На второй строке мы пытаемся вызвать message.


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


  • Является ли переменная message вызываемой?


  • Имеет ли она свойство toLowerCase?


  • Если имеет, является ли toLowerCase вызываемым?


  • Если оба этих значения являются вызываемыми, то что они возвращают?



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


Допустим, message была определена следующим образом:


const message = 'Hello World'

Как вы, наверное, догадались, при запуске message.toLowerCase() мы получим ту же строку, только в нижнем регистре.


Что насчет второй строки кода? Если вы знакомы с JS, то знаете, что в этом случае будет выброшено исключение:


TypeError: message is not a function// Ошибка типа: message  это не функция

Было бы здорово, если бы мы имели возможность избегать подобных ошибок.


При запуске нашего кода, способ, с помощью которого движок JS определяет, что делать, заключается в выяснении типа (type) значения каким поведением и возможностями он обладает. На это намекает TypeError она говорит, что строка 'Hello World' не может вызываться как функция.


Для некоторых значений, таких как примитивы string и number, мы можем определить их тип во время выполнения кода (runtime) с помощью оператора typeof. Но для других значений, таких как функции, соответствующий механизм для определения типов во время выполнения отсутствует. Например, рассмотрим следующую функцию:


function fn(x) {return x.flip()}

Читая этот код, мы можем сделать вывод, что функция будет работать только в случае передачи ей объекта с вызываемым свойством flip, но JS не обладает этой информацией. Единственным способом определить, что делает fn с определенным значением, в чистом JS является вызов этой функции. Такой вид поведения затрудняет предсказание поведения кода во время его написания.


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


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


Проверка статических типов


Вернемся к TypeError, которую мы получили, пытаясь вызвать string как функцию. Никто не любит получать ошибки или баги (bugs) при выполнении кода.


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


const message = 'Hello!'message()// This expression is not callable. Type 'String' has no call signatures. Данное выражение не является вызываемым. Тип 'String' не обладает сигнатурами вызова

При использовании TS, мы получаем ошибку перед выполнением кода (на этапе компиляции).


Ошибки, не являющиеся исключениями


До сих пор мы говорили об ошибках времени выполнения случаях, когда движок JS сообщает нам о том, что произошло нечто с его точки зрения бессмысленое. Спецификация ECMAScript содержит конкретные инструкции относительно того, как должен вести себя код при столкновении с чем-то неожиданным.


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


const user = {name: 'John',age: 30}user.location // undefined

В TS это, как и ожидается, приводит к ошибке:


const user = {name: 'John',age: 30}user.location// Property 'location' does not exist on type '{ name: string; age: number; }'. Свойства 'location' не существует в типе...

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


Например:


  • опечатки

const announcement = Hello World!;// Как быстро вы заметите опечатку?announcement.toLocaleLowercase();announcement.toLocalLowerCase();// Вероятно, мы хотели написать этоannouncement.toLocaleLowerCase();

  • функции, которые не были вызваны

function flipCoin() {// Должно было быть `Math.random()`return Math.random < 0.5;// Operator '<' cannot be applied to types '() => number' and 'number'. Оператор '<' не может быть применен к типам...}

  • или логические ошибки

const value = Math.random() < 0.5 ? a : b;if (value !== a) {// ...} else if (value === b) {// This condition will always return 'false' since the types 'a' and 'b' have no overlap. Данное условие будет всегда возвращать 'false', поскольку типы 'a' и 'b' не пересекаются// Упс, недостижимый участок кода}

Типы, интегрированные в среду разработки


TS защищает нас от совершения ошибок. Как он это делает? Все просто. Поскольку TS обладает информацией о системе типов, используемых в нашем коде, он начинает предполагать (делать вывод относительно того), какое свойство мы хотим использовать. Это означает, что TS показывает сообщения об ошибках и варианты завершения в процессе написания кода. Редактор кода, поддерживающий TS, также может предлагать способы быстрого исправления ошибок, предоставлять средства для автоматического рефакторинга, т.е. для легкой реорганизации кода, а также для полезной навигации, например, для быстрого перехода к определениям переменных или для поиска ссылок на переменную и т.д.


tsc, компилятор TS


Для начала установим tsc:


yarn global add tsc# илиnpm i -g tsc

Создадим файл hello.ts:


// Приветствуем всех собравшихсяconsole.log('Hello World!')

И скомпилируем (преобразуем) его в JS:


tsc hello.ts

Отлично. Мы не получили сообщений об ошибках в терминале, следовательно, компиляция прошла успешно. Заглянем в текущую директорию. Мы видим, что там появился файл hello.js. Этот файл является идентичным по содержанию файлу hello.ts, поскольку в данном случае TS нечего было преобразовывать. Кроме того, компилятор старается сохранять код максимально близким к тому, что написал разработчик.


Теперь попробуем вызвать ошибку. Перепишем hello.ts:


function greet(person, date) {console.log(`Hello, ${person}! Today is ${date}.`)}greet('John')

Если мы снова запустим tsc hello.ts, то получим ошибку:


Expected 2 arguments, but got 1. Ожидалось 2 аргумента, а получен 1

TS сообщает нам о том, что мы забыли передать аргумент в функцию greet, и он прав.


Компиляция с ошибками


Вы могли заметить, что после компиляции кода, содержащего ошибку, файл hello.js все равно обновился. Это объясняется тем, что TS считает вас умнее себя. Это также не мешает работающему JS-коду, при наличии некоторых ошибок, связанных с типами, благополучно работать дальше при постепенном переносе проекта на TS. Однако, если вы хотите, чтобы TS был более строгим, то можете указать флаг --noEmitOnError. Попробуйте снова изменить hello.ts и скомпилировать его с помощью такой команды:


tsc --noEmitOnError hello.ts

Вы увидите, что hello.js больше не обновляется.


Явные типы


Давайте отредактируем код и сообщим TS, что person это string, а date объект Date. Мы также вызовем метод toDateString() на date:


function greet(person: string, date: Date) {console.log(`Hello, ${person}! Today is ${date.toDateString().}`)}

То, что мы сделали, называется добавлением аннотаций типа (type annotations) к person и date для описания того, с какими типами значений может вызываться greet.


После этого TS будет сообщать нам о неправильных вызовах функции, например:


function greet(person: string, date: Date) {console.log(`Hello, ${person}! Today is ${date.toDateString()}.`);}greet('John', Date());// Argument of type 'string' is not assignable to parameter of type 'Date'. Аргумент типа 'string' не может быть присвоен параметру типа 'Date'

Вызов Date() возвращает строку. Для того, чтобы получить объект Date, следует вызвать new Date():


greet('John', new Date());

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


const msg = 'Hello!'// const msg: string

Удаление типов


Давайте скомпилируем функцию greet в JS с помощью tsc. Вот что мы получаем:


use strict;function greet(person, date) {console.log(Hello  + person + ! Today is  + date.toDateString() + .);}greet(John, new Date());

Обратите внимание на две вещи:


  1. Наши параметры person и date больше не имеют аннотаций типа.


  2. Наша шаблонная строка строка, в которой используются обратные кавычки (символ ```) была преобразована в обычную строку с конкатенациями (+).



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


Понижение уровня кода


Процесс, который часто называют понижением уровня кода (downleveling), состоит в преобразовании кода в код более старой версии, например, JS-кода, соответствующего спецификации ECMAScript 2015 (ES6), в код, соответствующий спецификации ECMAScript 3 (ES3). Шаблонные литералы (или шаблонные строки) были представлены в ES6, а TS по умолчанию преобразует код в ES3, поэтому наша шаблонная строка превратилась в обычную строку с объединениями. Для изменения спецификации, которой должен соответствовать компилируемый код, используется флаг --target. Например, команда tsc --target es2015 hello.ts оставит нашу строку неизменной.


Строгость


Строгость проверок, выполняемых TS, определяется несколькими флагами. Флаг --strict или настройка "strict": true в tsconfig.json включает максимальную строгость. Двумя другими главными настройками, определяющими строгость проверок, являются noImplicitAny и strictNullChecks.


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


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





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


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


Подробнее..

Перевод Карманная книга по TypeScript. Часть 2. Типы на каждый день

30.05.2021 10:22:52 | Автор: admin

image


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

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



Примитивы: string, number и boolean


В JS часто используется 3 примитива: string, number и boolean. Каждый из них имеет соответствующий тип в TS:


  • string представляет строковые значения, например, 'Hello World'
  • number предназначен для чисел, например, 42. JS не различает целые числа и числа с плавающей точкой (или запятой), поэтому не существует таких типов, как int или float только number
  • boolean предназначен для двух значений: true и false

Обратите внимание: типы String, Number и Boolean (начинающиеся с большой буквы) являются легальными и ссылаются на специальные встроенные типы, которые, однако, редко используются в коде. Для типов всегда следует использовать string, number или boolean.


Массивы


Для определения типа массива [1, 2, 3] можно использовать синтаксис number[]; такой синтаксис подходит для любого типа (например, string[] это массив строк и т.д.). Также можно встретить Array<number>, что означает тоже самое. Такой синтаксис, обычно, используется для определения общих типов или дженериков (generics).


Обратите внимание: [number] это другой тип, кортеж (tuple).


any


TS предоставляет специальный тип any, который может использоваться для отключения проверки типов:


let obj: any = { x: 0 }// Ни одна из строк ниже не приведет к возникновению ошибки на этапе компиляции// Использование `any` отключает проверку типов// Использование `any` означает, что вы знакомы со средой выполнения кода лучше, чем `TS`obj.foo()obj()obj.bar = 100obj = 'hello'const n: number = obj

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


noImplicitAny


При отсутствии определения типа и когда TS не может предположить его на основании контекста, неявным типом значение становится any.


Обычно, мы хотим этого избежать, поскольку any является небезопасным с точки зрения системы типов. Установка флага noImplicitAny позволяет квалифицировать любое неявное any как ошибку.


Аннотации типа для переменных


При объявлении переменной с помощью const, let или var опционально можно определить ее тип:


const myName: string = 'John'

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


// В аннотации типа нет необходимости - `myName` будет иметь тип `string`const myName = 'John'

Функции


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


Аннотации типа параметров


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


function greet(name: string) { console.log(`Hello, ${name.toUpperCase()}!`)}

Вот что произойдет при попытке вызвать функцию с неправильным аргументом:


greet(42)// Argument of type 'number' is not assignable to parameter of type 'string'. Аргумент типа 'number' не может быть присвоен параметру типа 'string'

Обратите внимание: количество передаваемых аргументов будет проверяться даже при отсутствии аннотаций типа параметров.


Аннотация типа возвращаемого значения


Также можно аннотировать тип возвращаемого функцией значения:


function getFavouriteNumber(): number { return 26}

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


Анонимные функции


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


Вот пример:


// Аннотации типа отсутствуют, но это не мешает `TS` обнаруживать ошибкиconst names = ['Alice', 'Bob', 'John']// Определение типов на основе контекста вызова функцииnames.forEach(function (s) { console.log(s.toUppercase()) // Property 'toUppercase' does not exist on type 'string'. Did you mean 'toUpperCase'? Свойства 'toUppercase' не существует в типе 'string'. Вы имели ввиду 'toUpperCase'?})// Определение типов на основе контекста также работает для стрелочных функцийnames.forEach((s) => { console.log(s.toUppercase()) // Property 'toUppercase' does not exist on type 'string'. Did you mean 'toUpperCase'?})

Несмотря на отсутствие аннотации типа для s, TS использует типы функции forEach, а также предполагаемый тип массива для определения типа s. Этот процесс называется определением типа на основе контекста (contextual typing).


Типы объекта


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


function printCoords(pt: { x: number, y: number }) { console.log(`Значение координаты 'x': ${pt.x}`) console.log(`Значение координаты 'y': ${pt.y}`)}printCoords({ x: 3, y: 7 })

Для разделения свойств можно использовать , или ;. Тип свойства является опциональным. Свойство без явно определенного типа будет иметь тип any.


Опциональные свойства


Для определения свойства в качестве опционального используется символ ? после названия свойства:


function printName(obj: { first: string, last?: string }) { // ...}// Обе функции скомпилируются без ошибокprintName({ first: 'John' })printName({ first: 'Jane', last: 'Air' })

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


function printName(obj: { first: string, last?: string }) { // Ошибка - приложение может сломаться, если аргумент `last` не будет передан в функцию console.log(obj.last.toUpperCase()) // Object is possibly 'undefined'. Потенциальным значением объекта является 'undefined' if (obj.last !== undefined) {   // Теперь все в порядке   console.log(obj.last.toUpperCase()) } // Безопасная альтернатива, использующая современный синтаксис `JS` - оператор опциональной последовательности (`?.`) console.log(obj.last?.toUpperCase())}

Объединения (unions)


Обратите внимание: в литературе, посвященной TS, union, обычно, переводится как объединение, но фактически речь идет об альтернативных типах, объединенных в один тип.


Определение объединения


Объединение это тип, сформированный из 2 и более типов, представляющий значение, которое может иметь один из этих типов. Типы, входящие в объединение, называются членами (members) объединения.


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


function printId(id: number | string) { console.log(`Ваш ID: ${id}`)}// OKprintId(101)// OKprintId('202')// ОшибкаprintId({ myID: 22342 })// Argument of type '{ myID: number }' is not assignable to parameter of type 'string | number'. Type '{ myID: number }' is not assignable to type 'number'. Аргумент типа '{ myID: number }' не может быть присвоен параметру типа 'string | number'. Тип '{ myID: number }' не может быть присвоен типу 'number'

Работа с объединениями


В случае с объединениями, TS позволяет делать только такие вещи, которые являются валидными для каждого члена объединения. Например, если у нас имеется объединение string | number, мы не сможем использовать методы, которые доступны только для string:


function printId(id: number | string) { console.log(id.toUpperCase()) // Property 'toUpperCase' does not exist on type 'string | number'. Property 'toUpperCase' does not exist on type 'number'.}

Решение данной проблемы заключается в сужении (narrowing) объединения. Например, TS знает, что только для string оператор typeof возвращает 'string':


function printId(id: number | string) { if (typeof id === 'string') {   // В этой ветке `id` имеет тип 'string'   console.log(id.toUpperCase()) } else {   // А здесь `id` имеет тип 'number'   console.log(id) }}

Другой способ заключается в использовании функции, такой как Array.isArray:


function welcomePeople(x: string[] | string) { if (Array.isArray(x)) {   // Здесь `x` - это 'string[]'   console.log('Привет, ' + x.join(' и ')) } else {   // Здесь `x` - 'string'   console.log('Добро пожаловать, одинокий странник ' + x) }}

В некоторых случаях все члены объединения будут иметь общие методы. Например, и массивы, и строки имеют метод slice. Если каждый член объединения имеет общее свойство, необходимость в сужении отсутствует:


function getFirstThree(x: number[] | string ) { return x.slice(0, 3)}

Синонимы типов (type aliases)


Что если мы хотим использовать один и тот же тип в нескольких местах? Для этого используются синонимы типов:


type Point = { x: number y: number}// В точности тоже самое, что в приведенном выше примереfunction printCoords(pt: Point) { console.log(`Значение координаты 'x': ${pt.x}`) console.log(`Значение координаты 'y': ${pt.y}`)}printCoords({ x: 3, y: 7 })

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


type ID = number | string

Обратите внимание: синонимы это всего лишь синонимы, мы не можем создавать на их основе другие "версии" типов. Например, такой код может выглядеть неправильным, но TS не видит в нем проблем, поскольку оба типа являются синонимами одного и того же типа:


type UserInputSanitizedString = stringfunction sanitizeInput(str: string): UserInputSanitizedString { return sanitize(str)}// Создаем "обезвреженный" инпутlet userInput = sanitizeInput(getInput())// По-прежнему имеем возможность изменять значение переменнойuserInput = 'new input'

Интерфейсы


Определение интерфейса это другой способ определения типа объекта:


interface Point { x: number y: number}function printCoords(pt: Point) { console.log(`Значение координаты 'x': ${pt.x}`) console.log(`Значение координаты 'y': ${pt.y}`)}printCoords({ x: 3, y: 7 })

TS иногда называют структурно типизированной системой типов (structurally typed type system) TS заботит лишь соблюдение структуры значения, передаваемого в функцию printCoords, т.е. содержит ли данное значение ожидаемые свойства.


Разница между синонимами типов и интерфейсами


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


Пример расширения интерфейса:


interface Animal { name: string}interface Bear extends Animal { honey: boolean}const bear = getBear()bear.namebear.honey

Пример расширения типа с помощью пересечения (intersection):


type Animal { name: string}type Bear = Animal & { honey: boolean}const bear = getBear()bear.namebear.honey

Пример добавления новых полей в существующий интерфейс:


interface Window { title: string}interface Window { ts: TypeScriptAPI}const src = 'const a = 'Hello World''window.ts.transpileModule(src, {})

Тип не может быть изменен после создания:


type Window = { title: string}type Window = { ts: TypeScriptAPI}// Ошибка: повторяющийся идентификатор 'Window'.

Общее правило: используйте interface до тех пор, пока вам не понадобятся возможности type.


Утверждение типа (type assertion)


В некоторых случаях мы знаем о типе значения больше, чем TS.


Например, когда мы используем document.getElementById, TS знает лишь то, что данный метод возвращает какой-то HTMLElement, но мы знаем, например, что будет возвращен HTMLCanvasElement. В этой ситуации мы можем использовать утверждение типа для определения более конкретного типа:


const myCanvas = document.getElementById('main_canvas') as HTMLCanvasElement

Для утверждения типа можно использовать другой синтаксис (е в TSX-файлах):


const myCanvas = <HTMLCanvasElement>document.getElementById('main_canvas')

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


const x = 'hello' as number// Conversion of type 'string' to type 'number' may be a mistake because neither type sufficiently overlaps with the other. If this was intentional, convert the expression to 'unknown' first.// Преобразование типа 'string' в тип 'number' может быть ошибкой, поскольку эти типы не перекрываются. Если это было сделано намерено, то выражение сначала следует преобразовать в 'unknown'

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


const a = (expr as any) as T

Литеральные типы (literal types)


В дополнение к общим типам string и number, мы можем ссылаться на конкретные строки и числа, находящиеся на определенных позициях.


Вот как TS создает типы для литералов:


let changingString = 'Hello World'changingString = 'Ol Mundo'// Поскольку `changingString` может представлять любую строку, вот// как TS описывает ее в системе типовchangingString // let changingString: stringconst constantString = 'Hello World'// Поскольку `constantString` может представлять только указанную строку, она// имеет такое литеральное представление типаconstantString // const constantString: 'Hello World'

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


let x: 'hello' = 'hello'// OKx = 'hello'// ...x = 'howdy'// Type '"howdy"' is not assignable to type '"hello"'.

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


function printText(s: string, alignment: 'left' | 'right' | 'center') { // ...}printText('Hello World', 'left')printText("G'day, mate", "centre")// Argument of type '"centre"' is not assignable to parameter of type '"left" | "right" | "center"'.

Числовые литеральные типы работают похожим образом:


function compare(a: string, b: string): -1 | 0 | 1 { return a === b ? 0 : a > b ? 1 : -1}

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


interface Options { width: number}function configure(x: Options | 'auto') { // ...}configure({ width: 100 })configure('auto')configure('automatic')// Argument of type '"automatic"' is not assignable to parameter of type 'Options | "auto"'.

Предположения типов литералов


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


const obj = { counter: 0 }if (someCondition) { obj.counter = 1}

TS не будет считать присвоение значения 1 полю, которое раньше имело значение 0, ошибкой. Это объясняется тем, что TS считает, что типом obj.counter является number, а не 0.


Тоже самое справедливо и в отношении строк:


const req = { url: 'https://example.com', method: 'GET' }handleRequest(req.url, req.method)// Argument of type 'string' is not assignable to parameter of type '"GET" | "POST"'.

В приведенном примере предположительный типом req.method является string, а не 'GET'. Поскольку код может быть вычислен между созданием req и вызовом функции handleRequest, которая может присвоить req.method новое значение, например, GUESS, TS считает, что данный код содержит ошибку.


Существует 2 способа решить эту проблему.


  1. Можно утвердить тип на каждой позиции:

// Изменение 1const req = { url: 'https://example.com', method: 'GET' as 'GET' }// Изменение 2handleRequest(req.url, req.method as 'GET')

  1. Для преобразования объекта в литерал можно использовать as const:

const req = { url: 'https://example.com', method: 'GET' } as consthandleRequest(req.url, req.method)

null и undefined


В JS существует два примитивных значения, сигнализирующих об отсутствии значения: null и undefined. TS имеет соответствующие типы. То, как эти типы обрабатываются, зависит от настройки strictNullChecks (см. часть 1).


Оператор утверждения ненулевого значения (non-null assertion operator)


TS предоставляет специальный синтаксис для удаления null и undefined из типа без необходимости выполнения явной проверки. Указание ! после выражения означает, что данное выражение не может быть нулевым, т.е. иметь значение null или undefined:


function liveDangerously(x?: number | undefined) { // Ошибки не возникает console.log(x!.toFixed())}

Перечисления (enums)


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


Редко используемые примитивы


bigint


Данный примитив используется для представления очень больших целых чисел BigInt:


// Создание `bigint` с помощью функции `BigInt`const oneHundred: bigint = BigInt(100)// Создание `bigint` с помощью литерального синтаксисаconst anotherHundred: bigint = 100n

Подробнее о BigInt можно почитать здесь.


symbol


Данный примитив используется для создания глобально уникальных ссылок с помощью функции Symbol():


const firstName = Symbol('name')const secondName = Symbol('name')if (firstName === secondName) { // This condition will always return 'false' since the types 'typeof firstName' and 'typeof secondName' have no overlap. // Символы `firstName` и `lastName` никогда не будут равными}

Подробнее о символах можно почитать здесь.




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


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


Подробнее..

Перевод Карманная книга по 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. Часть 7. Классы

18.06.2021 12:04:53 | Автор: admin

image


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

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



Члены класса (class members)


Вот пример самого простого класса пустого:


class Point {}

Такой класс бесполезен, поэтому давайте добавим ему несколько членов.


Поля (fields)


Поле это открытое (публичное) и доступное для записи свойство класса:


class Point {x: numbery: number}const pt = new Point()pt.x = 0pt.y = 0

Аннотация типа является опциональной (необязательной), но неявный тип будет иметь значение any.


Поля могут иметь инициализаторы, которые автоматически запускаются при инстанцировании класса:


class Point {x = 0y = 0}const pt = new Point()// Вывод: 0, 0console.log(`${pt.x}, ${pt.y}`)

Как и в случае с const, let и var, инициализатор свойства класса используется для предположения типа этого свойства:


const pt = new Point()pt.x = '0'// Type 'string' is not assignable to type 'number'.// Тип 'string' не может быть присвоен типу 'number'

--strictPropertyInitialization


Настройка strictPropertyInitialization определяет, должны ли поля класса инициализироваться в конструкторе.


class BadGreeter {name: string// Property 'name' has no initializer and is not definitely assigned in the constructor.// Свойство 'name' не имеет инициализатора и ему не присваивается значения в конструкторе}class GoodGreeter {name: stringconstructor() {this.name = 'привет'}}

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


Если вы намерены инициализировать поле вне конструктора, можете использовать оператор утверждения определения присвоения (definite assignment assertion operator, !):


class OKGreeter {// Не инициализируется, но ошибки не возникаетname!: string}

readonly


Перед названием поля можно указать модификатор readonly. Это запретит присваивать полю значения за пределами конструктора.


class Greeter {readonly name: string = 'народ'constructor(otherName?: string) {if (otherName !== undefined) {this.name = otherName}}err() {this.name = 'не ok'// Cannot assign to 'name' because it is a read-only property.// Невозможно присвоить значение свойству 'name', поскольку оно является доступным только для чтения}}const g = new Greeter()g.name = 'тоже не ok'// Cannot assign to 'name' because it is a read-only property.

Конструкторы (constructors)


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


class Point {x: numbery: number// Обычная сигнатура с дефолтными значениямиconstructor(x = 0, y = 0) {this.x = xthis.y = y}}class Point {// Перегрузкиconstructor(x: number, y: string)constructor(s: string)constructor(xs: any, y?: any) {// ...}}

Однако, между сигнатурами конструктора класса и функции существует несколько отличий:


  • Конструкторы не могут иметь параметров типа это задача возлагается на внешнее определение класса, о чем мы поговорим позже


  • Конструкторы не могут иметь аннотацию возвращаемого типа всегда возвращается тип экземпляра класса



super


Как и в JS, при наличии базового класса в теле конструктора, перед использованием this необходимо вызывать super():


class Base {k = 4}class Derived extends Base {constructor() {// В ES5 выводится неправильное значение, в ES6 выбрасывается исключениеconsole.log(this.k)// 'super' must be called before accessing 'this' in the constructor of a derived class.// Перед доступом к 'this' в конструкторе или производном классе необходимо вызвать 'super'super()}}

В JS легко забыть о необходимости вызова super, в TS почти невозможно.


Методы (methods)


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


class Point {x = 10y = 10scale(n: number): void {this.x *= nthis.y *= n}}

Как видите, TS не добавляет к методам ничего нового.


Обратите внимание, что в теле метода к полям и другим методам по-прежнему следует обращаться через this. Неквалифицированное название (unqualified name) в теле функции всегда будет указывать на лексическое окружение:


let x: number = 0class C {x: string = 'привет'm() {// Здесь мы пытаемся изменить значение переменной `x`, находящейся на первой строке, а не свойство классаx = 'world'// Type 'string' is not assignable to type 'number'.}}

Геттеры/сеттеры


Классы могут иметь акцессоры (вычисляемые свойства, accessors):


class C {_length = 0get length() {return this._length}set length(value) {this._length = value}}

TS имеет несколько специальных правил, касающихся предположения типов в случае с акцессорами:


  • Если set отсутствует, свойство автоматически становится readonly


  • Параметр типа сеттера предполагается на основе типа, возвращаемого геттером


  • Если параметр сеттера имеет аннотацию типа, она должна совпадать с типом, возвращаемым геттером


  • Геттеры и сеттеры должны иметь одинаковую видимость членов (см. ниже)



Если есть геттер, но нет сеттера, свойство автоматически становится readonly.


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


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


class MyClass {[s: string]: boolean | ((s: string) => boolean)check(s: string) {return this[s] as boolean}}

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


Классы и наследование


Как и в других объектно-ориентированных языках, классы в JS могут наследовать членов других классов.


implements


implements используется для проверки соответствия класса определенному interface. При несоответствии класса интерфейсу возникает ошибка:


interface Pingable {ping(): void}class Sonar implements Pingable {ping() {console.log('пинг!')}}class Ball implements Pingable {// Class 'Ball' incorrectly implements interface 'Pingable'. Property 'ping' is missing in type 'Ball' but required in type 'Pingable'.// Класс 'Ball' некорректно реализует интерфейс 'Pingable'. Свойство 'ping' отсутствует в типе 'Ball', но является обязательным в типе 'Pingable'pong() {console.log('понг!')}}

Классы могут реализовывать несколько интерейсов одновременно, например, class C implements A, B {}.


Предостережение


Важно понимать, что implements всего лишь проверяет, соответствует ли класс определенному интерфейсу. Он не изменяет тип класса или его методов. Ошибочно полагать, что implements изменяет тип класса это не так!


interface Checkable {check(name: string): boolean}class NameChecker implements Checkable {check(s) {// Parameter 's' implicitly has an 'any' type.// Неявным типом параметра 's' является 'any'// Обратите внимание, что ошибки не возникаетreturn s.toLowercse() === 'ok'// any}}

В приведенном примере мы, возможно, ожидали, что тип s будет определен на основе name: string в check. Это не так implements не меняет того, как проверяется тело класса или предполагаются его типы.


Также следует помнить о том, что определение в интерфейсе опционального свойства не приводит к созданию такого свойства:


interface A {x: numbery?: number}class C implements A {x = 0}const c = new C()c.y = 10// Property 'y' does not exist on type 'C'.// Свойства с названием 'y' не существует в типе 'C'

extends


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


class Animal {move() {console.log('Moving along!')}}class Dog extends Animal {woof(times: number) {for (let i = 0; i < times; i++) {console.log('woof!')}}}const d = new Dog()// Метод базового классаd.move()// Метод производного классаd.woof(3)

Перезапись методов


Производный класс может перезаписывать свойства и методы базового класса. Для доступа к методам базового класса можно использовать синтаксис super. Поскольку классы в JS это всего лишь объекты для поиска (lookup objects), такого понятия как супер-поле не существует.


TS обеспечивает, чтобы производный класс всегда был подтипом базового класса.


Пример легального способа перезаписи метода:


class Base {greet() {console.log('Привет, народ!')}}class Derived extends Base {greet(name?: string) {if (name === undefined) {super.greet()} else {console.log(`Привет, ${name.toUpperCase()}`)}}}const d = new Derived()d.greet()d.greet('читатель!')

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


// Создаем синоним для производного экземпляра с помощью ссылки на базовый классconst b: Base = d// Все работаетb.greet()

Что если производный класс не будет следовать конракту базового класса?


class Base {greet() {console.log('Привет, народ!')}}class Derived extends Base {// Делаем этот параметр обязательнымgreet(name: string) {// Property 'greet' in type 'Derived' is not assignable to the same property in base type 'Base'. Type '(name: string) => void' is not assignable to type '() => void'.// Свойство 'greet' в типе 'Derived' не может быть присвоено одноименному свойству в базовом типе 'Base'...console.log(`Привет, ${name.toUpperCase()}`)}}

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


const b: Base = new Derived()// Не работает, поскольку `name` имеет значение `undefined`b.greet()

Порядок инициализации


Порядок инициализации классов может быть неожиданным. Рассмотрим пример:


class Base {name = 'базовый'constructor() {console.log('Меня зовут ' + this.name)}}class Derived extends Base {name = 'производный'}// Вывод: 'базовый', а не 'производный'const d = new Derived()

Что здесь происходит?


Порядок инициализации согласно спецификации следующий:


  • Инициализация полей базового класса


  • Запуск конструктора базового класса


  • Инициализация полей производного класса


  • Запуск конструктора производного класса



Это означает, что конструктор базового класса использует собственное значение name, поскольку поля производного класса в этот момент еще не инициализированы.


Наследование встроенных типов


В ES2015 конструкторы, неявно возвращающие объекты, заменяют значение this для любого вызова super. Для генерируемого конструктора важно перехватывать потенциальное значение, возвращаемое super, и заменять его значением this.


Поэтому подклассы Error, Array и др. могут работать не так, как ожидается. Это объясняется тем, что Error, Array и др. используют new.target из ES6 для определения цепочки прототипов; определить значение new.target в ES5 невозможно. Другие компиляторы, обычно, имеют такие же ограничения.


Для такого подкласса:


class MsgError extends Error {constructor(m: string) {super(m)}sayHello() {return 'Привет ' + this.message}}

вы можете обнаружить, что:


  • методы объектов, возвращаемых при создании подклассов, могут иметь значение undefined, поэтому вызов sayHello завершится ошибкой


  • instanceof сломается между экземплярами подкласса и их экземплярами, поэтому (new MsgError()) instanceof MsgError возвращает false



Для решения данной проблемы можно явно устанавливать прототип сразу после вызова super.


class MsgError extends Error {constructor(m: string) {super(m)// Явно устанавливаем прототипObject.setPrototypeOf(this, MsgError.prototype)}sayHello() {return 'Привет ' + this.message}}

Тем не менее, любой подкласс MsgError также должен будет вручную устанавливать прототип. В среде выполнения, в которой не поддерживается Object.setPrototypeOf, можно использовать __proto__.


Видимость членов (member visibility)


Мы можем использовать TS для определения видимости методов и свойств для внешнего кода, т.е. кода, находящегося за пределами класса.


public


По умолчанию видимость членов класса имеет значение public. Публичный член доступен везде:


class Greeter {public greet() {console.log('Привет!')}}const g = new Greeter()g.greet()

Поскольку public является дефолтным значением, специально указывать его не обязательно, но это повышает читаемость и улучшает стиль кода.


protected


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


class Greeter {public greet() {console.log('Привет, ' + this.getName())}protected getName() {return 'народ!'}}class SpecialGreeter extends Greeter {public howdy() {// Здесь защищенный член доступенconsole.log('Здорово, ' + this.getName())}}const g = new SpecialGreeter()g.greet() // OKg.getName()// Property 'getName' is protected and only accessible within class 'Greeter' and its subclasses.// Свойство 'getName' является защищенным и доступно только в классе 'Greeter' и его подклассах

Раскрытие защищенных членов


Производные классы должны следовать контракту базового класса, но могут расширять подтипы базового класса дополнительными возможностями. Это включает в себя перевод protected членов в статус public:


class Base {protected m = 10}class Derived extends Base {// Модификатор отсутствует, поэтому значением по умолчанию является `public`m = 15}const d = new Derived()console.log(d.m) // OK

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


Доступ к защищенным членам за пределами иерархии классов


Разные языки ООП по-разному подходят к доступу к защищенным членам из базового класса:


class Base {protected x: number = 1}class Derived1 extends Base {protected x: number = 5}class Derived2 extends Base {f1(other: Derived2) {other.x = 10}f2(other: Base) {other.x = 10// Property 'x' is protected and only accessible through an instance of class 'Derived2'. This is an instance of class 'Base'.// Свойство 'x' является защищенным и доступно только через экземпляр класса 'Derived2'. А это  экземпляр класса 'Base'}}

Java, например, считает такой подход легальным, а C# и C++ нет.


TS считает такой подход нелегальным, поскольку доступ к x из Derived2 должен быть легальным только в подклассах Derived2, а Derived1 не является одним из них.


private


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


class Base {private x = 0}const b = new Base()// Снаружи класса доступ получить нельзяconsole.log(b.x)// Property 'x' is private and only accessible within class 'Base'.class Derived extends Base {showX() {// В подклассе доступ получить также нельзяconsole.log(this.x)// Property 'x' is private and only accessible within class 'Base'.}}

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


class Base {private x = 0}class Derived extends Base {// Class 'Derived' incorrectly extends base class 'Base'. Property 'x' is private in type 'Base' but not in type 'Derived'.// Класс 'Derived' неправильно расширяет базовый класс 'Base'. Свойство 'x' является частным в типе 'Base', но не в типе 'Derived'x = 1}

Доступ к защищенным членам между экземплярами


Разные языки ООП также по-разному подходят к предоставлению доступа экземплярам одного класса к защищенным членам друг друга. Такие языки как Java, C#, C++, Swift и PHP разрешают такой доступ, а Ruby нет.


TS разрешает такой доступ:


class A {private x = 10public sameAs(other: A) {// Ошибки не возникаетreturn other.x === this.x}}

Предостережение


Подобно другим аспектам системы типов TS, private и protected оказывают влияние на код только во время проверки типов. Это означает, что конструкции вроде in или простой перебор свойств имеют доступ к частным и защищенным членам:


class MySafe {private secretKey = 12345}// В JS-файле...const s = new MySafe()// Вывод 12345console.log(s.secretKey)

Для реализации настоящих частных членов можно использовать такие механизмы, как замыкания (closures), слабые карты (weak maps) или синтаксис приватных полей класса (private fields, #).


Статические члены (static members)


В классах могут определеяться статические члены. Такие члены не связаны с конкретными экземплярами класса. Они доступны через объект конструктора класса:


class MyClass {static x = 0static printX() {console.log(MyClass.x)}}console.log(MyClass.x)MyClass.printX()

К статическим членам также могут применяться модификаторы public, protected и private:


class MyClass {private static x = 0}console.log(MyClass.x)// Property 'x' is private and only accessible within class 'MyClass'.

Статические члены наследуются:


class Base {static getGreeting() {return 'Привет, народ!'}}class Derived extends Base {myGreeting = Derived.getGreeting()}

Специальные названия статических членов


Изменение прототипа Function считается плохой практикой. Поскольку классы это функции, вызываемые с помощью new, некоторые слова нельзя использовать в качестве названий статических членов. К таким словам относятся, в частности, свойства функций name, length и call:


class S {static name = 'S!'// Static property 'name' conflicts with built-in property 'Function.name' of constructor function 'S'.// Статическое свойство 'name' вступает в конфликт со встроенным свойством 'Function.name' функции-конструктора 'S'}

Почему не существует статических классов?


В некоторых языках, таких как C# или Java существует такая конструкция, как статический класс (static class).


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


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


// Ненужный статический классclass MyStaticClass {static doSomething() {}}// Альтернатива 1function doSomething() {}// Альтернатива 2const MyHelperObject = {dosomething() {},}

Общие классы (generic classes)


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


class Box<Type> {contents: Typeconstructor(value: Type) {this.contents = value}}const b = new Box('Привет!')// const b: Box<string>

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


Параметр типа в статических членах


Следующий код, как ни странно, является НЕлегальным:


class Box<Type> {static defaultValue: Type// Static members cannot reference class type parameters.// Статические члены не могут ссылаться на типы параметров класса}

Запомните, что типы полностью удаляются! Во время выполнения существует только один слот Box.defaultValue. Это означает, что установка Box<string>.defaultValue (если бы это было возможным) изменила бы Box<number>.defaultValue, что не есть хорошо. Поэтому статические члены общих классов не могут ссылаться на параметры типа класса.


Значение this в классах во время выполнения кода


TS не изменяет поведения JS во время выполнения. Обработка this в JS может показаться необычной:


class MyClass {name = 'класс'getName() {return this.name}}const c = new MyClass()const obj = {name: 'объект',getName: c.getName,}// Выводится 'объект', а не 'класс'console.log(obj.getName())

Если кратко, то значение this внутри функции зависит от того, как эта функция вызывается. В приведенном примере, поскольку функция вызывается через ссылку на obj, значением this является obj, а не экземпляр класса.


TS предоставляет некоторые средства для изменения такого поведения.


Стрелочные функции


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


class MyClass {name = 'класс'getName = () => {return this.name}}const c = new MyClass()const g = c.getName// Выводится 'класс'console.log(g())

Это требует некоторых компромиссов:


  • Значение this будет гарантированно правильным во время выполнения, даже в коде, не прошедшем проверки с помощью TS


  • Будет использоваться больше памяти, поскольку для каждого экземпляра класса будет создаваться новая функция


  • В производном классе нельзя будет использовать super.getName, поскольку отсутствует входная точка для получения метода базового класса в цепочке прототипов



Параметры this


При определении метода или функции начальный параметр под названием this имеет особое значение в TS. Данный параметр удаляется во время компиляции:


// TSfunction fn(this: SomeType, x: number) {/* ... */}// JSfunction fn(x) {/* ... */}

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


class MyClass {name = 'класс'getName(this: MyClass) {return this.name}}const c = new MyClass()// OKc.getName()// Ошибкаconst g = c.getNameconsole.log(g())// The 'this' context of type 'void' is not assignable to method's 'this' of type 'MyClass'.// Контекст 'this' типа 'void' не может быть присвоен методу 'this' типа 'MyClass'

Данный подход также сопряжен с несколькими органичениями:


  • Мы все еще имеем возможность вызывать метод неправильно


  • Выделяется только одна функция для каждого определения класса, а не для каждого экземпляра класса


  • Базовые определения методов могут по-прежнему вызываться через super



Типы this


В классах специальный тип this динамически ссылается на тип текущего класса:


class Box {contents: string = ''set(value: string) {// (method) Box.set(value: string): thisthis.contents = valuereturn this}}

Здесь TS предполагает, что типом this является тип, возвращаемый set, а не Box. Создадим подкласс Box:


class ClearableBox extends Box {clear() {this.contents = ''}}const a = new ClearableBox()const b = a.set('привет')// const b: ClearableBox

Мы также можем использовать this в аннотации типа параметра:


class Box {content: string = ''sameAs(other: this) {return other.content === this.content}}

Это отличается от other: Box если у нас имеется производный класс, его метод sameAs будет принимать только другие экземпляры этого производного класса:


class Box {content: string = ''sameAs(other: this) {return other.content === this.content}}class DerivedBox extends Box {otherContent: string = '?'}const base = new Box()const derived = new DerivedBox()derived.sameAs(base)// Argument of type 'Box' is not assignable to parameter of type 'DerivedBox'. Property 'otherContent' is missing in type 'Box' but required in type 'DerivedBox'.

Основанные на this защитники типа


Мы можем использовать this is Type в качестве возвращаемого типа в методах классов и интерфейсах. В сочетании с сужением типов (например, с помощью инструкции if), тип целевого объекта может быть сведен к более конкретному Type.


class FileSystemObject {isFile(): this is FileRep {return this instanceof FileRep}isDirectory(): this is Directory {return this instanceof Directory}isNetworked(): this is Networked & this {return this.networked}constructor(public path: string, private networked: boolean) {}}class FileRep extends FileSystemObject {constructor(path: string, public content: string) {super(path, false)}}class Directory extends FileSystemObject {children: FileSystemObject[]}interface Networked {host: string}const fso: FileSystemObject = new FileRep('foo/bar.txt', 'foo')if (fso.isFile()) {fso.content// const fso: FileRep} else if (fso.isDirectory()) {fso.children// const fso: Directory} else if (fso.isNetworked()) {fso.host// const fso: Networked & FileSystemObject}

Распространенным случаем использования защитников или предохранителей типа (type guards) на основе this является ленивая валидация определенного поля. В следующем примере мы удаляем undefined из значения, содержащегося в box, когда hasValue проверяется на истинность:


class Box<T> {value?: ThasValue(): this is { value: T } {return this.value !== undefined}}const box = new Box()box.value = 'Gameboy'box.value// (property) Box<unknown>.value?: unknownif (box.hasValue()) {box.value// (property) value: unknown}

Свойства параметров


TS предоставляет специальный синтаксис для преобразования параметров конструктора в свойства класса с аналогичными названиями и значениями. Это называется свойствами параметров (или параметризованными свойствами), такие свойства создаются с помощью добавления модификаторов public, private, protected или readonly к аргументам конструктора. Создаваемые поля получают те же модификаторы:


class Params {constructor(public readonly x: number,protected y: number,private z: number) {// ...}}const a = new Params(1, 2, 3)console.log(a.x)// (property) Params.x: numberconsole.log(a.z)// Property 'z' is private and only accessible within class 'Params'.

Выражения классов (class expressions)


Выражения классов похожи на определения классов. Единственным отличием между ними является то, что выражения классов не нуждаются в названии, мы можем ссылаться на них с помощью любого идентификатора, к которому они привязаны (bound):


const someClass = class<Type> {content: Typeconstructor(value: Type) {this.content = value}}const m = new someClass('Привет, народ!')// const m: someClass<string>

Абстрактные классы и члены


Классы, методы и поля в TS могут быть абстрактными.


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


Абстрактные классы выступают в роли базовых классов для подклассов, которые реализуют абстрактных членов. При отсутствии абстрактных членов класс считается конкретным (concrete).


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


abstract class Base {abstract getName(): stringprintName() {console.log('Привет, ' + this.getName())}}const b = new Base()// Cannot create an instance of an abstract class.// Невозможно создать экземпляр абстрактного класса

Мы не можем инстанцировать Base с помощью new, поскольку он является абстрактным. Вместо этого, мы должны создать производный класс и реализовать всех абстрактных членов:


class Derived extends Base {getName() {return 'народ!'}}const d = new Derived()d.printName()

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


class Derived extends Base {// Non-abstract class 'Derived' does not implement inherited abstract member 'getName' from class 'Base'.// Неабстрактный класс 'Derived' не реализует унаследованный от класса 'Base' абстрактный член 'getName'// Забыли про необходимость реализации абстрактных членов}

Сигнатуры абстрактных конструкций (abstract construct signatures)


Иногда нам требуется конструктор класса, создающий экземпляр класса, производный от некоторого абстрактного класса.


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


function greet(ctor: typeof Base) {const instance = new ctor()// Cannot create an instance of an abstract class.instance.printName()}

TS сообщает нам о том, что мы пытаемся создать экземпляр абстрактного класса. Тем не менее, имея определение greet, мы вполне можем создать абстрактный класс:


// Плохо!greet(Base)

Вместо этого, мы можем написать функцию, которая принимает нечто с сигнатурой конструктора:


function greet(ctor: new () => Base) {const instance = new ctor()instance.printName()}greet(Derived)greet(Base)/*Argument of type 'typeof Base' is not assignable to parameter of type 'new () => Base'.Cannot assign an abstract constructor type to a non-abstract constructor type.*//*Аргумент типа 'typeof Base' не может быть присвоен параметру типа 'new () => Base'.Невозможно присвоить тип абстрактного конструктора типу неабстрактного конструктора*/

Теперь TS правильно указывает нам на то, какой конструктор может быть вызван Derived может, а Base нет.


Отношения между классами


В большинстве случаев классы в TS сравниваются структурно, подобно другим типам.


Например, следующие два класса являются взаимозаменяемыми, поскольку они идентичны:


class Point1 {x = 0y = 0}class Point2 {x = 0y = 0}// OKconst p: Point1 = new Point2()

Также существуют отношения между подтипами, даже при отсутствии явного наследования:


class Person {name: stringage: number}class Employee {name: stringage: numbersalary: number}// OKconst p: Person = new Employee()

Однако, существует одно исключение.


Пустые классы не имеют членов. В структурном отношении такие классы являются супертипами для любых других типов. Так что, если мы создадим пустой класс (не надо этого делать!), вместо него можно будет использовать что угодно:


class Empty {}function fn(x: Empty) {// С `x` можно делать что угодно}// OK!fn(window)fn({})fn(fn)



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


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


Подробнее..

2 февральских вебинара по созданию бизнес-приложений

04.02.2021 10:09:11 | Автор: admin

Ранее в течение последней недели мы уже рассказывали о февральских мероприятиях по Microsoft 365 и Azure, а теперь пришло время статьи про мероприятия по созданию бизнес-приложений. В феврале их будет 2, оба про основы наших сервисов. Кстати, важный момент: после завершения каждого из этих вебинаров вы сможете бесплатно сдать профильные экзамены и получить сертификации. Присоединяйтесь!

1. Основы Microsoft Power Platform

10 февраля, на английском с субтитрами на русском.

Вебинар "Виртуальный учебный день Microsoft Power Platform: Основы" охватит все, что вам нужно знать о создании инновационных бизнес-решений и управлении ими, чтобы ваша организация была готова ко всему, что произойдет в будущем. Вы узнаете, как связать свои данные для анализа эффективности бизнеса в режиме реального времени, действовать на основе данных и аналитики с помощью специально созданных приложений, а также автоматизировать рабочие процессы для улучшения работы людей как удаленно, так и в офисе. Этот тренинг научит вас создавать инновационные, перспективные решения для меняющейся бизнес-среды.

Во время этого учебного мероприятия вы узнаете, как:

  • Подготовиться к сертификационному экзамену по основам Microsoft Power Platform

  • Создавать инновационные бизнес-решения и управлять ими с помощью Power Platform

  • Легко подключать все свои данные для анализа эффективности бизнеса с помощью специализированных приложений

  • Автоматизировать рабочие процессы для улучшения повседневных процессов независимо от ваших технических знаний

После завершения этого бесплатного обучения вы сможете бесплатно сдать сертификационный экзамен по основам Microsoft Power Platform.

Подробности и регистрация.

2. Основы Microsoft Dynamics 365

24 и 25 февраля, на английском с субтитрами на русском.

Получите навыки, необходимые для того, чтобы помочь вашей организации продавать, обслуживать и соответствовать ожиданиям клиентов на "Microsoft Dynamics 365 Virtual Training Day: Fundamentals". Во время этого бесплатного мероприятия, состоящего из двух частей, вы узнаете, как Dynamics 365 может помочь повысить эффективность, стимулируя инновации и поддерживая индивидуальный подход как для сотрудников, так и для клиентов. С помощью этих инструментов создание комплексной бизнес-платформы, выходящей за рамки управления взаимоотношениями с клиентами (CRM) и планирования ресурсов предприятия (ERP), никогда не было таким простым. Откройте для себя новые возможности масштабирования, более умные связи и используйте встроенный интеллект для более глубокого понимания каждой функциональной области вашего бизнеса, независимо от того, работаете ли вы удаленно или в офисе.

Во время этого учебного мероприятия вы узнаете, как:

  • Подготовиться к сертификационному экзамену по основам Microsoft Dynamics 365

  • Создать единую комплексную платформу для своего бизнеса, которая работает так же удаленно, как и в офисе

  • Использовать приложения на основе моделей для повышения эффективности масштабирования

  • Поддерживать индивидуальный подход как для сотрудников, так и для клиентов.

После прохождения этого бесплатного обучения вы сможете бесплатно сдать сертификационный экзамен по основам Microsoft Dynamics 365.

Подробности и регистрация.

Подробнее..

Microsoft Azure Virtual Training Days Fundamentals вебинар с конкурсом на бесплатное прохождение сертификации

11.08.2020 16:15:49 | Автор: admin


Подготовьтеськпереходувоблако.Узнайте,какMicrosoft Azure поддерживает безопасность, конфиденциальность и соответствиенормативнымтребованиям, а также подготовьтесь к сертификационному экзамену поосновамMicrosoft Azure.

ПоитогамкурсавсеучастникиполучатваучернапрохождениеэкзаменаAZ-900: Microsoft Azure Fundamentals.

17-18августа,Регистрация

Под катом немного подробностей (на английском).

Monday, August 17th, 2020, 10:00-13:25 | (GMT+02:00)
Tuesday, August 18th, 2020, 10:00-13:25 |(GMT+02:00)
Please note: this event will be delivered in English with closed captioning provided in English.

To create your vision for tomorrow, you need to understand what the cloud can do for you and your company today. In this introductory course, Microsoft Azure Virtual Training Day: Fundamentals, you will learn about cloud computing concepts, models and services, covering topics such as public, private and hybrid cloud, as well as infrastructure as a service, platform as a service and software as a service.

During this training event, you will explore how to:

  • Get started with Azure
  • Integrate Azure with your existing networks
  • Better understand key cloud concepts and core services, including pricing, support and cloud security

After completing this free training, youll be eligible to take theMicrosoft Azure Fundamentals certification examat no cost.

Heres what you can expect:
Introduction Introduction
Module 0: Course Introduction
Module 1: Cloud Concepts
Module 2: Security, Privacy, Compliance & Trust
Break: 10 minutes Break: 10 minutes
Module 3: Core Azure Services Module 4: Azure Pricing and Support
Closing Closing

The Microsoft Azure Virtual Training Day: Fundamentals event and associated vouchers are open to the public and offered at no cost. Prior to registering for this training, government employees must check with their employers to ensure their participation is permitted and in accordance with applicable policies and laws.
Подробнее..

Категории

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

  • Имя: Макс
    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