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

Книга по typescript

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


Подробнее..

Категории

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

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