Перейти к содержанию

Система типов в TypeScript

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

  • Система типов в TypeScript спроектирована быть необязательной, так чтобы ваш JavaScript был TypeScript.
  • TypeScript не блокирует генерацию JavaScript при наличии ошибок типов, что позволяет вам постепенно обновлять JS до TS.

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

Основные описания

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

В следующем примере демонстрируются описания типов для переменных, параметров функции и возвращаемых значений функции:

1
2
3
4
var num: number = 123;
function identity(num: number): number {
    return num;
}

Простые типы

Простые типы JavaScript хорошо представлены в системе типов TypeScript. Это означает обработку string, number, boolean как показано ниже:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
var num: number;
var str: string;
var bool: boolean;

num = 123;
num = 123.456;
num = '123'; // Ошибка

str = '123';
str = 123; // Ошибка

bool = true;
bool = false;
bool = 'false'; // Ошибка

Массивы

TypeScript предоставляет особый синтаксис типов для массивов, чтобы вам было легче описывать и документировать свой код. Синтаксис в основном заключается в добавлении постфикса [] к любому валидному описанию типа (например, :boolean[]). Это позволяет вам безопасно выполнять любые манипуляции с массивами, которые вы обычно делаете, и защищает вас от ошибок, таких как присвоение неправильного типа. Это продемонстрировано ниже:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
var boolArray: boolean[];

boolArray = [true, false];
console.log(boolArray[0]); // true
console.log(boolArray.length); // 2
boolArray[1] = true;
boolArray = [false, false];

boolArray[0] = 'false'; // Ошибка!
boolArray = 'false'; // Ошибка!
boolArray = [true, 'false']; // Ошибка!

Интерфейсы

Интерфейсы - это основной способ в TypeScript объединять описания нескольких типов в одно именованное описание. Рассмотрим следующий пример:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
interface Name {
    first: string;
    second: string;
}

var name: Name;
name = {
    first: 'John',
    second: 'Doe',
};

name = {
    // Ошибка : `second` отсутствует
    first: 'John',
};
name = {
    // Ошибка : `second` имеет неправильный тип
    first: 'John',
    second: 1337,
};

Здесь мы соединили описание first: string + second: string в новое описание Name, которое обеспечивает проверку типов отдельных элементов. Интерфейсы имеют большой потенциал в TypeScript, и мы посвятим целый раздел тому, как вы можете использовать это в своих интересах.

Встроенное описания типа

Вместо создания нового interface вы можете описать все, что вы хотите встроенно, используя :{ /*Structure*/ }. Предыдущий пример представлен снова уже как встроенный тип:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
var name: {
    first: string;
    second: string;
};
name = {
    first: 'John',
    second: 'Doe',
};

name = {
    // Ошибка : `second` отсутствует
    first: 'John',
};
name = {
    // Ошибка : `second` имеет неправильный тип
    first: 'John',
    second: 1337,
};

Встроенные типы отлично подходят для быстрого однократного предоставления описания для чего-либо. Это избавляет вас от необходимости придумывать (потенциально плохое) имя типа. Однако, если вы обнаружите, что вставляете одно и то же встраиваемое описание типа несколько раз, неплохо было бы подумать о рефакторинге его в интерфейс (или псевдоним типа, описанный далее в этом разделе).

Специальные типы

Помимо рассмотренных простых типов, есть несколько типов, которые имеют особое значение в TypeScript. Это any, null, undefined, void.

any

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

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
var power: any;

// Принимает все типы
power = '123';
power = 123;

// Совместим со всеми типами
var num: number;
power = num;
num = power;

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

null или undefined

То, как они обрабатываются системой типов, зависит от флага компилятора strictNullChecks (мы рассмотрим этот флаг позже). Когда strictNullCheck:false, литералы JavaScript null и undefined эффективно обрабатываются системой типов как что-то наподобие any. Эти литералы могут быть назначены любому другому типу. Это продемонстрировано в следующем примере:

1
2
3
4
5
6
var num: number;
var str: string;

// Эти литералы могут быть назначены на что угодно
num = null;
str = undefined;

:void

Используйте :void, чтобы описать, что функция ничего не возвращает:

1
2
3
function log(message): void {
    console.log(message);
}

Дженерики

Многие алгоритмы и структуры данных в информатике не полагаются на текущий тип объекта. Тем не менее, вы все еще хотите наложить ограничения между различными переменными. Простым мини-примером является функция, которая принимает список элементов и возвращает перевернутый список элементов. Здесь есть ограничение между тем, что передается функции, и тем, что возвращается функцией:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
function reverse<T>(items: T[]): T[] {
    var toreturn = [];
    for (let i = items.length - 1; i >= 0; i--) {
        toreturn.push(items[i]);
    }
    return toreturn;
}

var sample = [1, 2, 3];
var reversed = reverse(sample);
console.log(reversed); // 3,2,1

// Защита!
reversed[0] = '1'; // Ошибка!
reversed = ['1', '2']; // Ошибка!

reversed[0] = 1; // Хорошо
reversed = [1, 2]; // Хорошо

Здесь вы по сути говорите, что функция reverse принимает массив (items: T[]) элементов какого-то типа T (обратите внимание на параметр типа в reverse<T>) и возвращает массив элементов типа T (обратите внимание : T[]). Поскольку функция reverse возвращает элементы того же типа, что и приняла, TypeScript знает, что переменная reversed также имеет тип number[] и обеспечит вам защиту типа. Точно так же, если вы передаете массив string[] в функцию reverse, возвращаемый результат также является массивом string[], и вы получаете защиту типов, аналогичную показанной ниже:

1
2
3
4
var strArr = ['1', '2'];
var reversedStrs = reverse(strArr);

reversedStrs = [1, 2]; // Ошибка!

На самом деле JavaScript массивы уже имеют функцию .reverse, а TypeScript фактически использует дженерики для определения её структуры:

1
2
3
4
interface Array<T> {
    reverse(): T[];
    // ...
}

Это означает, что вы получаете защиту типов при вызове .reverse для любого массива, как показано ниже:

1
2
3
4
var numArr = [1, 2];
var reversedNums = numArr.reverse();

reversedNums = ['1', '2']; // Ошибка!

Мы обсудим больше интерфейс Array<T> позже, когда расскажем про lib.d.ts в разделе Объявления окружения.

Тип объединения

Как правило, в JavaScript вы хотите, чтобы свойство было одним из нескольких типов, например string или number. Здесь пригодится тип объединения (обозначаемый через | в описании типа, например, string|number). Обычный вариант использования - это функция, которая может принимать одиночный элемент или массив элементов, например:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
function formatCommandline(command: string[] | string) {
    var line = '';
    if (typeof command === 'string') {
        line = command.trim();
    } else {
        line = command.join(' ').trim();
    }

    // Делаем всякие штуки со строкой: строка
}

Тип пересечения

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

1
2
3
4
5
6
7
8
9
function extend<T, U>(first: T, second: U): T & U {
    return { ...first, ...second };
}

const x = extend({ a: 'hello' }, { b: 42 });

// x теперь имеет и `a`, и` b`
const a = x.a;
const b = x.b;

Тип кортежа

В JavaScript нет поддержки кортежей. Разработчики обычно используют массив в качестве кортежа. Но зато система типов TypeScript поддерживает кортежи. Кортежи могут быть описаны с помощью : [typeofmember1, typeofmember2] и т.д. Кортеж может иметь любое количество элементов. Кортежи демонстрируются в следующем примере:

1
2
3
4
5
6
7
var nameNumber: [string, number];

// Хорошо
nameNumber = ['Jenny', 8675309];

// Ошибка!
nameNumber = ['Jenny', '867-5309'];

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

1
2
3
4
var nameNumber: [string, number];
nameNumber = ['Jenny', 8675309];

var [name, num] = nameNumber;

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

TypeScript предоставляет удобный синтаксис для указания имен описания типов, которые вы хотели бы использовать более чем в одном месте. Псевдонимы создаются с использованием синтаксиса type SomeName = someValidTypeAnnotation. Пример демонстрируется ниже:

1
2
3
4
5
6
7
8
9
type StrOrNum = string | number;

// Использование: как и любая другая запись
var sample: StrOrNum;
sample = 123;
sample = '123';

// Просто проверяю
sample = true; // Ошибка!

В отличие от interface, вы можете дать псевдоним типа буквально для описания любого типа (полезно для таких вещей, как типы объединения и пересечения). Вот еще несколько примеров, чтобы вы познакомились с синтаксисом:

1
2
3
type Text = string | { text: string };
type Coordinates = [number, number];
type Callback = (data: string) => void;

СОВЕТ: Если вам нужны иерархии описаний типа, используйте interface. Их можно использовать с implements и extends

СОВЕТ: используйте псевдоним типа для более простых структур объектов (например, Coordinates), просто чтобы дать им семантическое имя. Также, когда вы хотите дать семантические имена для типов объединения или пересечения, псевдонимы типов - это верный способ.

Заключение

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

Комментарии