Примитивы: string, number и boolean
В JS часто используется 3 примитива: string, number и boolean. Каждый из них имеет соответствующий тип в TS:
stringпредставляет строковые значения, например,'Hello World'numberпредназначен для чисел, например,42.JSне различает целые числа и числа с плавающей точкой (или запятой), поэтому не существует таких типов, какintилиfloat— толькоnumberboolean— предназначен для двух значений: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 — это другой способ определения типа объекта:
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
const req = { url: 'https://example.com', method: 'GET' as 'GET' }
// Изменение 2
handleRequest(req.url, req.method as 'GET')- Для преобразования объекта в литерал можно использовать
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` никогда не будут равными
}Подробнее о символах можно почитать здесь.