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

Kate

Administrator
Команда форума
Мы продолжаем серию публикаций адаптированного и дополненного перевода "Карманной книги по 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` никогда не будут равными
}

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



Источник статьи: https://habr.com/ru/company/macloud/blog/559976/
 
Сверху