Spec-Zone.ru › TypeScript 5.1

Типы повседневного использования

В этой главе мы рассмотрим некоторые из наиболее распространённых типов значений, которые вы найдёте в коде JavaScript, и объясним соответствующие способы описания этих типов в TypeScript. Это не исчерпывающий список, и в будущих главах будут описаны дополнительные способы именования и использования других типов.

Типы также могут появляться во многих других местах, помимо аннотаций типов. По мере изучения самих типов мы также узнаем о местах, где мы можем сослаться на эти типы для формирования новых конструкций.

Мы начнём с обзора наиболее базовых и распространённых типов, с которыми вы можете столкнуться при написании кода JavaScript или TypeScript. Впоследствии они станут основополагающими строительными блоками более сложных типов.

Примитивные типы: string, number, и boolean

В JavaScript используются три очень часто встречающихся примитивных типа: string, number, и boolean. Каждый из них имеет соответствующий тип в TypeScript. Как можно ожидать, эти имена совпадают с именами, которые вы бы увидели, если бы использовали оператор JavaScript typeof на значении этих типов:

  • string представляет строковые значения, такие как "Hello, world"
  • number предназначен для чисел, таких как 42. В JavaScript нет специального значения выполнения для целых чисел, поэтому нет эквивалента int или float — всё это просто number
  • boolean предназначен для двух значений true и false

Имена типов String, Number, и Boolean (начинающиеся с заглавной буквы) являются допустимыми, но относятся к некоторым специальным встроенным типам, которые очень редко встречаются в вашем коде. Всегда используйте string, number, или boolean для типов.

Массивы

Для указания типа массива, например, [1, 2, 3], можно использовать синтаксис number[]; этот синтаксис работает для любого типа (например, string[] — массив строк и так далее). Вы также можете увидеть это как Array<number>, что означает то же самое. Мы узнаем больше о синтаксисе T<U> при изучении обобщений.

Обратите внимание, что [number] — это другое понятие; обратитесь к разделу о Кортежах.

any

В TypeScript также существует специальный тип any, который можно использовать всякий раз, когда вы не хотите, чтобы конкретное значение вызывало ошибки проверки типов.

Когда значение имеет тип any, вы можете получить доступ к любым свойствам этого значения (которые в свою очередь будут иметь тип any), вызвать его как функцию, присвоить его (или из него) значению любого типа или сделать практически всё, что синтаксически допустимо:

let obj: any = { x: 0 };
// None of the following lines of code will throw compiler errors.
// Using `any` disables all further type checking, and it is assumed 
// you know the environment better than TypeScript.
obj.foo();
obj();
obj.bar = 100;
obj = "hello";
const n: number = obj;

Тип any полезен, когда вы не хотите писать длинный тип только для того, чтобы убедить TypeScript в правильности определённой строки кода.

noImplicitAny

Когда вы не указываете тип, и TypeScript не может его вывести из контекста, компилятор обычно по умолчанию использует тип any.

Однако обычно этого следует избегать, так как тип any не проверяется по типам. Используйте флаг компилятора noImplicitAny, чтобы обозначить любой неявный тип any как ошибку.

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

При объявлении переменной с помощью const, var, или let, вы можете необязательно добавить аннотацию типа, чтобы явно указать тип переменной:

let myName: string = "Alice";

TypeScript не использует объявления типа «тип слева» типа int x = 0;. Аннотации типа всегда идут после того, что типизируется.

Однако в большинстве случаев это не требуется. Всякий раз, когда это возможно, TypeScript пытается автоматически вывести типы в вашем коде. Например, тип переменной выводится на основе типа её инициализатора:

// No type annotation needed -- 'myName' inferred as type 'string'
let myName = "Alice";

В основном вам не нужно явно изучать правила вывода. Начинающим рекомендуется использовать меньше аннотаций типов, чем вы думаете — вас может удивить, как мало их нужно, чтобы TypeScript полностью понял, что происходит.

Функции

Функции являются основным средством передачи данных в JavaScript. TypeScript позволяет указывать типы как входных, так и выходных значений функций.

Аннотации типов параметров

При объявлении функции вы можете добавить аннотации типов после каждого параметра, чтобы указать типы параметров, которые принимает функция. Аннотации типов параметров идут после имени параметра:

// Parameter type annotation
function greet(name: string) {
  console.log("Hello, " + name.toUpperCase() + "!!");
}

Когда параметр имеет аннотацию типа, аргументы этой функции будут проверены:

// Would be a runtime error if executed!
greet(42);

Даже если у параметров нет аннотаций типов, TypeScript по-прежнему проверит, что вы передали правильное количество аргументов.

Аннотации типов возвращаемых значений

Вы также можете добавить аннотации типов возвращаемых значений. Аннотации типов возвращаемых значений появляются после списка параметров:

function getFavoriteNumber(): number {
  return 26;
}

Подобно аннотациям типов переменных, аннотацию типа возвращаемого значения обычно не требуется, так как TypeScript выведет тип возвращаемого значения функции на основе её return операторов. Аннотация типа в приведённом выше примере ничего не изменяет. В некоторых кодовых базах явно указывается тип возвращаемого значения для документации, предотвращения случайных изменений или просто по предпочтению.

Анонимные функции

Анонимные функции немного отличаются от объявлений функций. Когда функция появляется в месте, где TypeScript может определить, как она будет вызвана, параметрам этой функции автоматически присваиваются типы.

Вот пример:

const names = ["Alice", "Bob", "Eve"];
 
// Contextual typing for function - parameter s inferred to have type string
names.forEach(function (s) {
  console.log(s.toUpperCase());
});
 
// Contextual typing also applies to arrow functions
names.forEach((s) => {
  console.log(s.toUpperCase());
});

Несмотря на то, что параметр s не имел аннотации типа, TypeScript использовал типы функции forEach, а также выведенный тип массива, чтобы определить, какой тип будет иметь s.

Этот процесс называется контекстной типизацией, так как контекст, в котором находилась функция, определяет, какой тип она должна иметь.

Подобно правилам вывода, вам не нужно явно изучать, как это происходит, но понимание того, что это происходит, может помочь вам заметить, когда аннотации типов не нужны. Позже мы увидим больше примеров того, как контекст, в котором находится значение, может влиять на его тип.

Объектные типы

Помимо примитивных типов, наиболее распространённым типом, с которым вы столкнетесь, является объектный тип. Это относится к любому значению JavaScript со свойствами, что касается практически всех значений! Чтобы определить объектный тип, мы просто перечислим его свойства и их типы.

Например, вот функция, которая принимает объект, похожий на точку:

// The parameter's type annotation is an object type
function printCoord(pt: { x: number; y: number }) {
  console.log("The coordinate's x value is " + pt.x);
  console.log("The coordinate's y value is " + pt.y);
}
printCoord({ x: 3, y: 7 });

Здесь мы отметили параметр типом с двумя свойствами — x и y — которые оба имеют тип number. Вы можете использовать , или ; для разделения свойств, и последний разделитель необязателен в обоих случаях.

Часть типа каждого свойства также необязательна. Если вы не указываете тип, он будет предполагаться как any.

Необязательные свойства

Объектные типы также могут указывать, что некоторые или все их свойства являются необязательными. Для этого добавьте ? после имени свойства:

function printName(obj: { first: string; last?: string }) {
  // ...
}
// Both OK
printName({ first: "Bob" });
printName({ first: "Alice", last: "Alisson" });

В JavaScript, если вы обращаетесь к свойству, которого не существует, вы получите значение undefined вместо ошибки выполнения. Из-за этого, когда вы читаете из необязательного свойства, вам нужно проверить наличие undefined перед использованием.

function printName(obj: { first: string; last?: string }) {
  // Error - might crash if 'obj.last' wasn't provided!
  console.log(obj.last.toUpperCase());
  if (obj.last !== undefined) {
    // OK
    console.log(obj.last.toUpperCase());
  }
 
  // A safe alternative using modern JavaScript syntax:
  console.log(obj.last?.toUpperCase());
}

Объединения типов

Система типов TypeScript позволяет создавать новые типы из существующих с помощью широкого спектра операторов. Теперь, когда мы знаем, как записать несколько типов, пришло время начать объединять их интересными способами.

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

Первый способ комбинирования типов, который вы можете увидеть, — это объединение типов. Объединение типов — это тип, образованный из двух или более других типов, представляющий значения, которые могут быть любым из этих типов. Каждый из этих типов называется членом объединения.

Давайте напишем функцию, которая может работать со строками или числами:

function printId(id: number | string) {
  console.log("Your ID is: " + id);
}
// OK
printId(101);
// OK
printId("202");
// Error
printId({ myID: 22342 });

Работа с объединениями типов

Легко предоставить значение, соответствующее типу объединения — просто предоставьте тип, соответствующий любому из членов объединения. Если у вас есть значение типа объединения, как с ним работать?

TypeScript позволит операцию только в том случае, если она допустима для каждого члена объединения. Например, если у вас есть объединение string | number, вы не можете использовать методы, доступные только для string:

function printId(id: number | string) {
  console.log(id.toUpperCase());
}

Решение состоит в том, чтобы сузить объединение с помощью кода, так же, как вы бы сделали в JavaScript без аннотаций типов. Сужение происходит, когда TypeScript может вывести более конкретный тип для значения на основе структуры кода.

Например, TypeScript знает, что только значение типа string будет иметь свойство typeof со значением "string":

function printId(id: number | string) {
  if (typeof id === "string") {
    // In this branch, id is of type 'string'
    console.log(id.toUpperCase());
  } else {
    // Here, id is of type 'number'
    console.log(id);
  }
}

Ещё один пример — использование функции типа Array.isArray:

function welcomePeople(x: string[] | string) {
  if (Array.isArray(x)) {
    // Here: 'x' is 'string[]'
    console.log("Hello, " + x.join(" and "));
  } else {
    // Here: 'x' is 'string'
    console.log("Welcome lone traveler " + x);
  }
}

Обратите внимание, что в ветке else нам не нужно ничего особенного — если x не являлось string[] , то оно должно было быть string.

Иногда у вас будет объединение, где у всех членов есть нечто общее. Например, у массивов и строк есть метод slice. Если у каждого члена в объединении есть общее свойство, вы можете использовать это свойство без сужения:

// Return type is inferred as number[] | string
function getFirstThree(x: number[] | string) {
  return x.slice(0, 3);
}

Может показаться непонятным, что объединение типов имеет пересечение свойств этих типов. Это не случайность — название «объединение» происходит из теории типов. Объединение number | string формируется путём объединения значений из каждого типа. Обратите внимание, что при наличии двух множеств с соответствующими фактами о каждом множестве, только пересечение этих фактов применимо к объединению самих множеств. Например, если у нас есть комната высоких людей в шляпах и другая комната испаноязычных людей в шляпах, то после объединения этих комнат единственное, что мы знаем о каждом человеке, — это то, что он должен носить шляпу.

Псевдонимы типов

Мы использовали типы объектов и объединения типов, записывая их непосредственно в аннотации типов. Это удобно, но часто требуется использовать один и тот же тип несколько раз и ссылаться на него по одному имени.

Псевдоним типа — это имя для любого типа. Синтаксис псевдонима типа:

type Point = {
  x: number;
  y: number;
};
 
// Exactly the same as the earlier example
function printCoord(pt: Point) {
  console.log("The coordinate's x value is " + pt.x);
  console.log("The coordinate's y value is " + pt.y);
}
 
printCoord({ x: 100, y: 100 });

Вы можете использовать псевдоним типа для присвоения имени любому типу, а не только типу объекта. Например, псевдоним типа может именовать тип объединения:

type ID = number | string;

Обратите внимание, что псевдонимы — это только псевдонимы. Вы не можете использовать псевдонимы типов для создания различных «версий» одного и того же типа. При использовании псевдонима это будет точно так же, как если бы вы записали алиасируемый тип. Другими словами, этот код может выглядеть незаконным, но в TypeScript он корректен, так как оба типа являются псевдонимами одного и того же типа:

type UserInputSanitizedString = string;
 
function sanitizeInput(str: string): UserInputSanitizedString {
  return sanitize(str);
}
 
// Create a sanitized input
let userInput = sanitizeInput(getInput());
 
// Can still be re-assigned with a string though
userInput = "new input";

Интерфейсы

Объявление интерфейса — это ещё один способ именования типа объекта:

interface Point {
  x: number;
  y: number;
}
 
function printCoord(pt: Point) {
  console.log("The coordinate's x value is " + pt.x);
  console.log("The coordinate's y value is " + pt.y);
}
 
printCoord({ x: 100, y: 100 });

Как и при использовании псевдонима типа выше, пример работает так же, как если бы мы использовали анонимный тип объекта. TypeScript интересует только структура значения, которое мы передали в printCoord — его интересуют только ожидаемые свойства. Именно потому, что TypeScript интересуется только структурой и возможностями типов, мы называем TypeScript системой типов со структурной типизацией.

Различия между псевдонимами типов и интерфейсами

Псевдонимы типов и интерфейсы очень похожи, и во многих случаях вы можете свободно выбирать между ними. Почти все возможности interface доступны в type, ключевое отличие заключается в том, что тип нельзя повторно открыть для добавления новых свойств, в отличие от интерфейса, который всегда можно расширить.

Interface Type

Расширение интерфейса

interface Animal {
  name: string;
}
interface Bear extends Animal {
  honey: boolean;
}
const bear = getBear();
bear.name;
bear.honey;
        

Расширение типа с помощью пересечений

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;
}
 // Error: Duplicate identifier 'Window'.
        

Вы узнаете больше об этих понятиях в последующих главах, поэтому не беспокойтесь, если вы не понимаете все это сразу.

  • До версии TypeScript 4.2 имена псевдонимов типов могут отображаться в сообщениях об ошибках, иногда вместо эквивалентного анонимного типа (что может быть или не быть желательным). Имена интерфейсов всегда будут указаны в сообщениях об ошибках.
  • Псевдонимы типов могут не участвовать в объединении деклараций, но интерфейсы могут.
  • Интерфейсы могут использоваться только для описания форм объектов, а не переименования примитивов.
  • Имена интерфейсов будут всегда отображаться в исходном виде в сообщениях об ошибках, но только тогда, когда они используются по имени.

В основном, вы можете выбирать в зависимости от личных предпочтений, и TypeScript сообщит вам, если потребуется другой тип объявления. Если вам нужен эвристический подход, используйте interface до тех пор, пока не потребуются функции из type.

Утверждения о типах

Иногда у вас есть информация о типе значения, которая неизвестна TypeScript.

Например, если вы используете document.getElementById, TypeScript знает только, что это вернёт какой-то HTMLElement, но вы можете знать, что на вашей странице всегда есть HTMLCanvasElement с заданным идентификатором.

В этом случае вы можете использовать утверждение о типе для указания более конкретного типа:

const myCanvas = document.getElementById("main_canvas") as HTMLCanvasElement;

Как и аннотация типа, утверждения о типах удаляются компилятором и не повлияют на поведение вашего кода во время выполнения.

Вы также можете использовать синтаксис с угловыми скобками (за исключением кода в файле .tsx), который эквивалентен:

const myCanvas = <HTMLCanvasElement>document.getElementById("main_canvas");

Напоминание: поскольку утверждения о типах удаляются во время компиляции, с ними не связана проверка во время выполнения. Если утверждение о типе неверно, не произойдёт исключения или null.

TypeScript допускает утверждения о типах, которые преобразуются в более конкретную или менее конкретную версию типа. Это правило предотвращает «невозможные» преобразования, такие как:

const x = "hello" as number;

Иногда это правило может быть слишком консервативным и запретить более сложные преобразования, которые могут быть допустимыми. Если это происходит, вы можете использовать два утверждения, сначала для any (или unknown, которое мы представим позже), а затем для желаемого типа:

const a = (expr as any) as T;

Литеральные типы

В дополнение к общим типам string и number, мы можем ссылаться на конкретные строки и числа в позициях типов.

Один из способов понять это — представить, как JavaScript имеет разные способы объявления переменной. Как var так и let позволяют изменять содержимое переменной, а const этого не делает. Это отражается на том, как TypeScript создаёт типы для литералов.

let changingString = "Hello World";
changingString = "Olá Mundo";
// Because `changingString` can represent any possible string, that
// is how TypeScript describes it in the type system
changingString;
 
const constantString = "Hello World";
// Because `constantString` can only represent 1 possible string, it
// has a literal type representation
constantString;

Сами по себе литеральные типы не очень полезны:

let x: "hello" = "hello";
// OK
x = "hello";
// ...
x = "howdy";

Не очень полезно иметь переменную, которая может принимать только одно значение!

Но путём комбинирования литералов в объединения вы можете выразить гораздо более полезную концепцию — например, функции, которые принимают только определённый набор известных значений:

function printText(s: string, alignment: "left" | "right" | "center") {
  // ...
}
printText("Hello, world", "left");
printText("G'day, mate", "centre");

Числовые литеральные типы работают аналогичным образом:

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");

Есть ещё один вид литерального типа: булевы литералы. Существует только два булевых литеральных типа, и, как вы, вероятно, догадались, это типы true и false. Тип boolean сам по себе фактически является псевдонимом для объединения true | false.

Вывод литералов

Когда вы инициализируете переменную объектом, TypeScript предполагает, что значения свойств этого объекта могут измениться в дальнейшем. Например, если вы написали код подобный этому:

const obj = { counter: 0 };
if (someCondition) {
  obj.counter = 1;
}

TypeScript не считает ошибкой присвоение 1 полю, которое ранее содержало 0. Другими словами, obj.counter должен иметь тип number, а не 0, потому что типы используются для определения поведения как при чтении, так и при записи.

То же самое относится к строкам:

declare function handleRequest(url: string, method: "GET" | "POST"): void;
 
const req = { url: "https://example.com", method: "GET" };
handleRequest(req.url, req.method);

В приведенном выше примере req.method выводится как string, а не как "GET". Поскольку код может быть оценён между созданием req и вызовом handleRequest, который мог бы присвоить новую строку, например, "GUESS" полю req.method, TypeScript считает этот код ошибочным.

Есть два способа обойти эту проблему.

  1. Вы можете изменить вывод, добавив утверждение о типе в любом месте:

    // Change 1:
    const req = { url: "https://example.com", method: "GET" as "GET" };
    // Change 2
    handleRequest(req.url, req.method as "GET");

    Изменение 1 означает «Я намерен, чтобы req.method всегда имел литеральный тип "GET", предотвращая возможное присвоение "GUESS" этому полю после. Изменение 2 означает «Я знаю по другим причинам, что req.method имеет значение "GET"».

  2. Вы можете использовать as const для преобразования всего объекта в литеральные типы:

    const req = { url: "https://example.com", method: "GET" } as const;
    handleRequest(req.url, req.method);

Суффикс as const действует как const, но для системы типов, гарантируя, что все свойства имеют литеральный тип, а не более общий тип, такой как string или number.

null и undefined

В JavaScript есть два примитивных значения, используемых для обозначения отсутствия или неинициализированного значения: null и undefined.

В TypeScript есть два соответствующих типа с одинаковыми именами. Как ведут себя эти типы зависит от того, включена ли у вас опция strictNullChecks.

strictNullChecks выключена

При выключенной опции strictNullChecks, к значениям, которые могут быть null или undefined, можно по-прежнему обращаться обычным способом, а значения null и undefined можно назначить свойству любого типа. Это похоже на то, как ведут себя языки без проверок на null (например, C#, Java). Отсутствие проверки на эти значения часто является основной причиной ошибок; мы всегда рекомендуем включать strictNullChecks, если это возможно в вашем коде.

strictNullChecks включена

При включенной опции strictNullChecks, если значение является null или undefined, перед использованием методов или свойств этого значения необходимо проверить эти значения. Так же, как проверка на undefined перед использованием необязательного свойства, мы можем использовать сужение для проверки значений, которые могут быть null.

function doSomething(x: string | null) {
  if (x === null) {
    // do nothing
  } else {
    console.log("Hello, " + x.toUpperCase());
  }
}

Оператор непустого утверждения (постфикс !)

TypeScript также имеет специальную синтаксическую конструкцию для удаления null и undefined из типа без явных проверок. Запись ! после любого выражения — это по сути утверждение типа, что значение не является null или undefined.

function liveDangerously(x?: number | null) {
  // No error
  console.log(x!.toFixed());
}

Как и другие утверждения типа, это не меняет поведения вашего кода во время выполнения, поэтому важно использовать ! только тогда, когда вы знаете, что значение не может быть null или undefined.

Перечисления

Перечисления — это функция, добавленная в JavaScript TypeScript, которая позволяет описать значение, которое может быть одним из набора возможных именованных констант. В отличие от большинства функций TypeScript, это не добавление на уровне типов в JavaScript, а нечто добавленное в сам язык и среду выполнения. Поэтому это функция, о существовании которой вы должны знать, но возможно, отложить её использование, пока вы не будете уверены. Подробнее о перечислениях можно прочитать на странице справки Перечисления.

Менее распространённые примитивы

Стоит упомянуть об остальных примитивах в JavaScript, которые представлены в системе типов. Хотя мы не будем углубляться в них здесь.

bigint

С ES2020 в JavaScript появился примитив для очень больших целых чисел, BigInt:

// Creating a bigint via the BigInt function
const oneHundred: bigint = BigInt(100);
 
// Creating a BigInt via the literal syntax
const anotherHundred: bigint = 100n;

Дополнительную информацию о BigInt можно найти в примечаниях к выпуску TypeScript 3.2.

symbol

В JavaScript есть примитив для создания глобально уникальной ссылки с помощью функции Symbol():

const firstName = Symbol("name");
const secondName = Symbol("name");
 
if (firstName === secondName) {
  // Can't ever happen
}

Дополнительную информацию можно найти на странице справки Символы.

© 2012-2023 Microsoft
Licensed under the Apache License, Version 2.0.
https://www.typescriptlang.org/docs/handbook/2/everyday-types.html

Spec-Zone.ru

Настройки Оффлайн Что нового Помощь О нас
Spec-Zone .ru
спецификации, руководства, описания, API