Typescript Flashcards
TypeScript преимущества
- Нахождение некоторых видов ошибок еще до запуска кода. В программировании языки делятся на две большие группы: динамически типизированные и статически типизированные. JavaScript относится к первой группе. У таких языков есть интерпретатор, программа, которая выполняет код построчно без предварительного анализа. Статически типизированные, к которым относится TypeScript, языки работают по-другому. Перед тем как запустить код этих языков на выполнение, его нужно скомпилировать. Во время компиляции проверяется, что программа типобезопасна, то есть она не содержит ошибок подобных примеру выше. Если компилятор нашел не соответствие типов, то он останавливает компиляцию и выводит предупреждения о том, где типы не сходятся.
- Более простой рефакторинг кода
- Полную поддержку возможностей редактора: автодополнения, навигации по коду и т.п.
- TypeScript обратно совместим с JavaScript. Любой код, написанный на JS будет выполнен. Также можно писать смешанный код и он будет валиден.
- Реализует многие принципы объектно-ориентированного программирования: модификаторы доступа, наследование, инкапсуляцию и полиморфизм. Есть система для работы с модулями, классами. Даже есть возможность создавать абстрактные классы.
Минусы:
Наличие дополнительных файлов (*.ts, *.d.ts, *.map), что неудобно для небольших проектов.
Для некоторых браузеров необходима дополнительная настройка консоли для отладки TypeScript.
TypeScript — язык с неявной статической типизацией: тип может быть описан как any, что отключит приведение к этому типу переменной.
TypeScript: Null и Undefined
В TypeScript null и undefined не просто значения, а два типа, состоящие из одного значения.
В TypeScript c правильной (strict) конфигурацией проверка на null встроена и статический анализатор скажет вам о возможной проблеме. Чтобы ее решить, также требуется написать соответствующее условие или использовать оператор ?, что позволяет избежать ошибок во время исполнения кода
function foo(value?: string | null) {
if (value !== null && value !== undefined) {
const upperValue = value.toUpperCase();
// ^? (parameter) value: string
}
// остальная логика
}
Это стало возможным как раз благодаря выделению значений null и undefined в отдельные типы, а благодаря Narrowing и Union Types не пришлось изобретать дополнительный механизм. И все решение укладывается в концепцию “типы как множества”. Благодаря каждой проверке мы отсекаем не подходящее нам множество значений и получаем безопасный вызов метода. Такие проверки также называются отсечением типов (Differentiating Types) и Type Guards.
TypeScript: Rest и Spread
Rest-оператор позволяет создавать функции с переменным числом параметров, сворачивая их в массив. В этом смысле rest-оператор в TypeScript ничем не отличается от rest-оператора в JavaScript.
function max(...numbers: number[]) {
return Math.max(...numbers);
}
Spread-оператор в функциях это как rest-оператор наоборот. Он позволяет раскладывать массив на отдельные параметры:const numbers = [1, 2, 3];
Math.max(...numbers);
Сложность:
Если функция принимает на вход любое количество аргументов, как в примере выше, то такой код работает без проблем, но если функция принимает на вход определенное число аргументов, то TypeScript выдаст ошибку компиляции:function sum(a: number, b: number) {
return a + b;
}
const args = [1, 2];
sum(...args);
Есть разные способы обойти это ограничение, но в данном случае проще всего использовать Type Assertion. Это указание компилятору того, что мы точно знаем о коде.const args = [1, 2] as const;
TypeScript: Аннотации типов (type annotations)
В случае составных типов, например если мы хотим использовать объединение или описание объекта добавляются скобки: (Type)[]:const users: ({ name: string })[] = [];
const users: (string | null)[] = [];
TypeScript дает еще один синтаксис, который описывается так: Array<Type>. Он универсальный и с его помощью можно описать любой массив. Форма Array нужна в первую очередь для дженериков.
`const users: Array<string> = [];
const users: Array<number> = [];
const users: Array<User> = [];
const users: Array<{ name: string }> = [];
const users: Array<string | null> = [];`</User></number></string></Type>
Если определить пустой массив без указания типа, то его типом автоматически станет any[]. В такой массив можно добавить любые данные, включая вложенные массивы. Код с any будет работать всегда, но он выключает проверку типов. Чтобы этого не происходило, нужно всегда явно типизировать пустой массив.const items = [];
items.push(1);
items.push('wow');
items.push(['code-basics', 'hexlet']);
TypeScript: Анонимные функции
Т.к. анонимные и стрелочные функции почти всегда используются в том же месте, где и определяются, TypeScript может вывести типы их параметров. Для определения таких функций указание типов опускают. Этот процесс называется контекстная типизация (contextual typing), так как контекст определения функции, позволяет вывести типы входных параметров.
const fruits = [‘banana’, ‘mango’, ‘apple’];
const upperFruits = fruits.map((name) => name.toUpperCase());
// [‘BANANA’, ‘MANGO’, ‘APPLE’]
Если функция определяется вне контекста, то к ней применяются те же правила, что и к именованным функциям, то есть типы параметров должны быть заданы во время определения.
const toUpper = (name: string): string => name.toUpperCase();
const upperFruits = fruits.map(toUpper);
TypeScript: Деструктуризация
Деструктуризация в определении функций - механизм, с помощью которого объект переданный как аргумент распаковывается и его части присваиваются локальным переменным функции.
// Обычное определение
function f(user: { firstName: string, age: number }) {
console.log(firstName, age);
}
// Деструктурированный объект
function f({ firstName, age }: { firstName: string, age: number }) {
console.log(firstName, age);
}
Деструктурированный объект все равно остается объектом, поэтому в TypeScript описание его типа идет после закрывающей фигурной скобки. Можно сделать код менее многословным вынеся определение типа в алиас. Все тоже самое применимо и к массивам.
// Деструктурированный массив
function foo([x, y]: number[]) {
console.log(x, y);
}
TypeScript: Иерархия типов (type hierarcy)
Иерархия типов в TypeScript представляет собой отношения между различными типами данных в языке. Она помогает организовать типы данных в логическую структуру и определяет их отношения и производные отношения.
В TypeScript есть несколько встроенных иерархий типов, которые образуются на основе отношений между типами данных. Вот некоторые из них:
- Примитивные типы: это базовые типы данных в TypeScript, такие как number, string, boolean, null, undefined, symbol.
- Объектные типы: это типы данных, которые представляют собой объекты в JavaScript, такие как объекты, массивы, функции и классы.
- Специальные типы: это типы данных, которые имеют особую семантику или поведение в TypeScript, такие как any, unknown, void, never.
- Дополнительные типы: это пользовательские типы данных, которые могут быть определены разработчиком. Это может быть простой пользовательский тип, такой как тип перечисления (enum), или составной пользовательский тип, такой как класс или интерфейс.
Иерархия типов в TypeScript позволяет использовать наследование и расширение типов данных, что облегчает разработку программного обеспечения и повышает читаемость и поддерживаемость кода.
TypeScript: Именованные функции
Функции требуют обязательного указания типов всех входных параметров. При таком указании, параметр будет обязательным. Попытка вызвать функцию без параметра приведет к ошибке компиляции.
function getGreetingPhrase(name: string) {
return Hello, ${name.toUpperCase()}!
;
}
Чтобы сделать параметр необязательным, нужно добавить знак ? после имени переменной. В таком случае тип переменной name становится составным (Union Type): string | undefined, что читается как “строка или undefined”. Необязательный параметр может быть undefined, но не null.
function getGreetingPhrase(name?: string) {
return Hello, ${name ? name.toUpperCase() : 'Guest'}!
;
}
Для добавления null нужно изменить определение так, выйдет string | undefined | null.
function getGreetingPhrase(name?: string | null) {
return Hello, ${name ? name.toUpperCase() : 'Guest'}!
;
}
Значение по умолчанию задается как в JavaScript. Сама переменная автоматически становится необязательной, и тип выводится исходя из переданного значения:
function getGreetingPhrase(name = ‘Guest’) {
return Hello, ${name.toUpperCase()}!
;
}
getGreetingPhrase() // Hello, Guest!
TypeScript выводит тип возвращаемого значения самостоятельно, но хорошим тоном считается указание его явно:
function getGreetingPhrase(name: string): string {
return Hello, ${name.toUpperCase()}!
;
}
TypeScript: Кортежи (Tuples)
Обычно массивы могут менять свой размер и содержать ноль или более значений, тем самым пустой массив как значение [] является валидным для массивов любого типа. Но иногда массивы выпуступают в качестве упрощенной версии объекта, где количество значений и их порядок строго определен. Например с помощью такого массива можно представить точку на плоскости: [x, y].
В TypeScript подобные массивы называются кортежами и у них есть свой собственный синтаксис определения. Кортежи могут состоять из элементов разных типов type HTTPResponse = [number, string], а часть из них может быть опциональна const HTTPResponse = [number, string?].
const point: [number, number] = [1, 3]
// Можно поменять
const point[0] = 4;
// Обращение к несуществующему индексу приведет к ошибке
point[5]; // Error!
// Нельзя создать не совпадающий по типу
const point2: [number, number] = [1, 3, 8]; // Error!
Обратите внимание на создание переменных для кортежей. Если используется алиас, то его нужно указывать явно, иначе, с точки зрения TypeScript будет создан обычный массив:
// все хорошо, использован алиас
const response2: HTTPResponse = [201, ‘Created’];
// Будет иметь тип (string | number)[]
const response = [201, ‘Created’];
Важно помнить, что к push() или pop() поведение не применяется, исторически так сложилось. Общая рекомендация состоит в том, чтобы не пытаться изменять размер кортежа.
point.push(10);
console.log(point); // [4, 3, 10];
TypeScript: Литералы (Literal Types)
TypeScript поддерживает литеральный тип для следующих типов: string, boolean, number и BigInt.
С точки зрения теории множеств, такой тип представляет собой множество, состоящее из одного элемента, а для системы типов это ограничение, что переменной не может быть присвоено ничего, кроме указанного значения:
type TestValue = ‘test’;
let test: TestValue = ‘test’;
test = ‘string’; // Error: Type ‘“string”’ is not assignable to type ‘“test”’.
Они сами по себе не очень практичны, но могут быть объединены для создания мощной (практической) абстракции в объединенном типе:
type CardinalDirection = ‘North’ | ‘East’ | ‘South’ | ‘West’;
function move(distance: number, direction: CardinalDirection) {
// …
}
move(1, ‘North’); // ok
move(1, ‘Nurth’); // Error
Также литеральные типы могут комбинироваться с любыми другими типами, так мы можем получить ограничение, под которое попадают все числа и false:
type NumberFalse = number | false;
Проблема, описанная в этом уроке, в большинстве языков реализуется через перечисления (Enum), но проблема в том, что перечисления — это конструкция языка, которая остается существовать в коде после трансляции кода в JavaScript. По этой причине многие разработчики выбирают вместо них Union Types, которые позволяют сделать практически то же самое с помощью простых типов.
В объектных литералах поля инициализируются одним литеральным типом или их пересечением:
type DataSourceOption = {
type: ‘postgre’ | ‘mysql’; // часто используется в библиотеках, огда от нас ожидают одну из двух разных строк.
host: string;
port: number;
}
В случае с объектами конфигурации часто мы не хотим, чтобы их меняли извне, и ожидаем конкретных значений внутри, здесь нам на помощь приходит приведение типа к литеральному через as const. На выходе мы получаем тип с неизменяемыми (readonly) полями и литеральными типами в значении. Такая техника также применима к массивам, превращая их в кортежи, и к примитивам, где вывод литерального типа не срабатывает автоматически.
const ormConfig = {
type: ‘mysql’;
host: ‘localhost’;
port: 5432;
} as const;
TypeScript: Массивы
TypeScript умеет выводить тип массива, но массив — это составной тип данных, который представляет собой контейнер для какого-то другого типа. К примеру, number[] - массив чисел, string[] - массив строк.
TypeScript: Массивы только для чтения
В TypeScript работа с неизменяемыми массивами встроена в систему типов. Чтобы гарантировать неизменяемость, массив помечается модификатором readonly. Он запрещает изменение массива, но не запрещает изменение объектов, находящихся внутри массива
function process(numbers: readonly number[]) {
numbers.push(1); // Error!
}
const items: readonly ({ key: string })[] = [{ key: ‘value’}];
items[0].key = ‘another value’; // ok!
Модификатор readonly, сам по себе является синтаксическим сахаром. Технически, в случае массива readonly меняет тип Array, на тип ReadonlyArray.
TypeScript: Многомерные массивы (multi dimensional arrays)
Для определения многомерных массивов используется синтаксис Type[][].
const items1 = [[3, 8], [10, 4, 8]]; // items1: number[][]
// Используя алиас
type User = {
name: string;
}
// или так Array<User[]>
const users: User[][] = [
[{ name: ‘Eva’}, { name: ‘Adam’ }],
];
Для определения массивов составных типов нужно использовать скобки.
const coll: (string | number)[][] = [];
coll.push([‘hexlet’, 5])
TypeScript: Объединения (Union Types)
Объединение (пересечение) типов играют большую роль в TypeScript, они позволяют выразить обычную для JavaScript ситуацию, когда возвращаемое значение или аргумент функции могут быть различного типа. Объединение указывается с помощью оператора прямой черты |, по обе стороны которого располагаются типы. В результате мы получаем тип, обещающий содержать переменную одного из типов объединения.
type at = (str: string, position: number) => string | undefined;
Union Types используется повсеместно, где программист хочет сказать, что переменная может содержать значения разных, но заранее описанных типов. Для указания абсолютно произвольных типов может использоваться unknown или более конкретные дженерики, которые рассмотрим дальше в курсе.
TypeScript: Объектные типы (Object Types)
Тип объекта состоит из типов всех входящих в него свойств. Он выводится автоматически:
// Тип: { firstName: string; pointsCount: number; }
const user = {
firstName: ‘Mike’,
pointsCount: 1000,
};
// Поменять тип свойств нельзя
// Type ‘number’ is not assignable to type ‘string’.
user.firstName = 7;
TypeScript не позволяет обращаться к несуществующим свойствам. Это значит, что структура любого объекта должна быть задана при его инициализации.
Как и в случае примитивных типов данных, ни null, ни undefined по умолчанию не разрешены. Чтобы изменить это поведение, нужно добавить опциональность:
// firstName может быть undefined
// pointsCount может быть null
function doSomething(user: { firstName?: string; pointsCount: number | null; }) {…}