Примитивы: stringnumber и boolean

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

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

Обратите внимание: типы StringNumber и Boolean (начинающиеся с большой буквы) являются легальными и ссылаются на специальные встроенные типы, которые, однако, редко используются в коде. Для типов всегда следует использовать stringnumber или 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 как ошибку.

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

При объявлении переменной с помощью constlet или 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'?
})

Несмотря на отсутствие аннотации типа для sTS использует типы функции 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)

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

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

Объединение — это тип, сформированный из 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 — это другой способ определения типа объекта:

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.getElementByIdTS знает лишь то, что данный метод возвращает какой-то 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 новое значение, например, GUESSTS считает, что данный код содержит ошибку.

Существует 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 и undefinedTS имеет соответствующие типы. То, как эти типы обрабатываются, зависит от настройки 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` никогда не будут равными
}

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