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

в 7:00, , рубрики: TypeScript, vds, vps, Блог компании Маклауд, книга по typescript, Лайфхаки для гиков, основы, Разработка веб-сайтов

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 = 100
obj = '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}`)
}

// OK
printId(101)
// OK
printId('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 = string

function 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.name
bear.honey

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

type Animal {
 name: string
}

type Bear = Animal & {
 honey: boolean
}

const bear = getBear()
bear.name
bear.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: string

const constantString = 'Hello World'
// Поскольку `constantString` может представлять только указанную строку, она
// имеет такое литеральное представление типа
constantString
 // const constantString: 'Hello World'

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

let x: 'hello' = 'hello'
// OK
x = '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. Можно утвердить тип на каждой позиции:

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

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

const req = { url: 'https://example.com', method: 'GET' } as const
handleRequest(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% скидку на первый месяц аренды сервера любой конфигурации!

Автор: owlofmacloud

Источник

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js