Введение в TypeScript
Основные типы
В TypeScript поддерживаются те же типы, что и в JavaScript, но с добавлением дополнительных типов.
Boolean, number, string
const isDone: boolean = false; // boolean
const n: number = 2; // number
const color: string = 'blue'; // string
Array
Тип массивов можно записать одним из двух способов. В первом используется тип элементов, за которым следует []
, чтобы
обозначить массив элементов этого типа:
const list: number[] = [1, 2, 3];
Во втором используется generic
тип, Array<elemType>
:
const list: Array<number> = [1, 2, 3];
Tuple
Тип tuple
позволяет обозначить массив с фиксированным числом элементов, типы которых известны, но не обязательно
должны быть одинаковыми.
// Декларируем тип tuple
let x = [string, number];
// Присваиваем значение
x = ['hello', 10]; // OK
// Присваиваем некорректное значение
x = [10, 'hello']; // Error
Enum
Enum
— это способ дать более понятные имена наборам числовых значений.
enum Color {
Red,
Green,
Blue,
}
const c: Color = Color.Green;
По умолчанию нумерация элементов enum
начинается с 0
. Но это можно изменить, установив значение вручную. Предыдущий
пример теперь начинается с 1
вместо 0
:
enum Color {
Red = 1,
Green,
Blue,
}
const c: Color = Color.Green;
Или можно установить вручную все значения:
enum Color {
Red = 1,
Green = 2,
Blue = 4,
}
const c: Color = Color.Green;
Unknown
С помощью unknown
удобно обозначать переменные, тип которых на данный момент не известен. TypeScript откажется
выполнять с такой переменной какие-либо действия.
Any
Тип, который выключает типизацию. То есть переменной можно присвоить любое значение, но TypeScript не сможет помочь в случае ошибки. Может быть полезно при использовании библиотек, которые написаны без использования TypeScript.
Void
void
— отсутствие какого-либо типа вообще. Используется как возвращаемый тип функций, которые ничего не возвращают:
function warnUser(): void {
console.log('This is my warning message!')
}
Never
Возвращаемый тип функции, как void
, но обозначает функцию, которая никогда не завершится:
// функция выкинет ошибку и не завершится
function neverFn(): never {
throw new Error('error');
}
// функция вызовет бесконечный цикл
function infiniteLoop(): never {
while (true) {}
}
Null и Undefined
В TypeScript null
и undefined
имеют типы null
и undefined
соответственно. Сами по себе они не особо полезны:
// Переменным можно присвоить только одно значение
const n: null = null;
const u: undefined = undefined;
Interfaces
С помощью интерфейсов удобно описывать объекты.
interface MyInterface {
a: string;
b: number[];
}
const myObj: MyInterface = {
a: 'hello!',
b: [1, 2, 3],
}
Optional properties
Не все свойства объекта могут быть обязательными. В интерфейсах необязательные свойства помечаются с помощью ?
:
interface MyInterface {
a?: string;
b: number[];
}
const myObj: MyInterface = {
// Проверка типа не выдаст ошибку из-за пропущенного свойства "a"
b: [1, 2, 3],
}
Readonly
Иногда нужно, чтобы свойства объекта нельзя было изменить. Для таких свойств используется ключевое слово readonly
:
interface MyInterface {
readonly a: string;
}
const myObj: MyInterface = {
a: 'hello!',
}
myObj.a = 'bye!'; // error
Index signature
Бывают ситуации, когда мы не знаем какой объект будет получен и сколько у него будет свойств. Известны только значения
этих свойств (например string
):
const apiAnswer = { key: 'q', key2: 'w', ..., key99: 'e' }
Для таких случаев используется index signature:
interface IndexInterface {
[n: string]: string;
}
const apiAnswer: IndexInterface = { key: 'q', key2: 'w', key99: 'e' }
// можем обратиться к любому ключу объекта, и это значение будет "string"
const value = apiAnswer.randomKey; // -> type: string
Functions
Типизация функции на примере calculate
, которая принимает метод (add
— сложить, или sub
— вычесть),
левую часть выражения и правую:
// method — строка
// left и rigth — числа
// возвращаемое значение функции — число
function calculate(method: 'add' | 'sub', left: number, right: number): number {
switch (method) {
case 'add': return left + right;
case 'sub': return left - right;
}
}
Помимо того, что можно типизировать аргументы функции и ее возвращаемое значение, можно еще дать тип самой функции:
// с помощью type alias:
type TypeFn = () => number;
const arrowFn: TypeFn = () => 2;
// с помощью interface:
interface FnInterface {
(a: string): void;
}
const fn: FnInterface = (a) => {
console.log(a)
}
Classes
TypeScript представляет полную поддержку ключевого слова class
введенного в ES2015.
Fields
Объявление поля создает общедоступное записываемое свойство класса:
class Point {
x: number;
y: number;
}
const pt = new Point();
pt.x = 0;
pt.y = 0;
Аннотация типа не обязательна. Если тип и значение не будут указаны явно, то тип такого поля будет any
. Если поле
инициализировано с начальным значением, но без явного типа, то TypeScript выведет тип автоматически:
class Point {
x = 0; // -> number
y; // -> any
}
--strictPropertyInitialization
Параметр --strictPropertyInitialization
определяет, нужно ли инициализировать поля класса в конструкторе:
class BadGreeter {
name: string;
// Error
// свойство `name` не имеет инициализатора
// и не определено в конструктуре
}
class GoodGreeter {
name: string;
constructor() {
this.name = 'hello';
}
}
readonly
Поля могут иметь модификатор readonly
. Таким полям нельзя присвоить значение вне конструктора.
class Greeter {
readonly name: string = 'world';
constructor(otherName?: string) {
if (otherName !== undefined) {
this.name = otherName;
}
}
err() {
this.name = 'not ok'; // error
// -> нельзя присвоить значение полю 'name'
// потому что это read-only поле
}
}
const g = new Greeter();
g.name = 'also not ok'; // error
Вызов super
Как и в JavaScript, если у вас есть базовый класс, то нужно вызвать super()
в теле функции конструктора прежде, чем
использовать this
:
class Base {
k = 4;
}
class Derived extends Base {
constructor() {
console.log(this.k); // Error
// перед обращением к 'this' необходимо вызвать 'super'
super();
}
}
Забыть вызвать super
— это распространенная ошибка, но TypeScript сообщит нам, когда это необходимо.
Methods
Свойство-функция в классе называется методом. Методы могут использовать аннотации тех же типов, что и функции и конструкторы:
class Point {
x = 10;
y = 10;
scale(n: number): void {
this.x *= n;
this.y *= n;
}
}
Наследование классов
Как и в других языках с объектно-ориентированными функциями, классы в JavaScript могут наследоваться от базовых классов.
implements
Мы можем использовать implements
, чтобы проверить, удовлетворяет ли класс определенному интерфейсу. Если класс не
может правильно реализовать интерфейс, будет выдана ошибка:
interface Pingable {
ping(): void;
}
class Sonar implements Pingable {
ping() {
console.log('ping!');
}
}
class Ball implements Pingable {
// error
pong() {
console.log('pong!');
}
}
Расширение классов. Extends
Классы могут происходить от базового класса. Производный класс имеет все свойства и методы своего базового класса, а так же определяет дополнительные свойства.
class Animal {
move() {
console.log('Moving along!');
}
}
class Dog extends Animal {
woof(times: number) {
for (let i = 0; i < times; i++) {
console.log('woof!');
}
}
}
const dog = new Dog();
dog.move(); // метод базового класса
dog.woof(3); // метод дочернего класса
Переопределение методов
Производный (дочерний) класс может переопределять поле или свойство базового класса.
class Base {
greet() {
console.log('Hello, world!');
}
}
class Derived extends Base {
greet(name?: string) {
if (name === undefined) {
super.greet();
} else {
console.log(`Hello, ${name}!`)
}
}
}
const d = new Derived();
d.greet(); // -> Hello, world!
d.greet('reader'); // -> Hello, reader!
Модификаторы доступа
Мы можем использовать TypeScript для управления отображением определенных методов и свойств для кода вне класса.
public
По умолчанию все свойства класса имеют уровень доступа public
. Получить доступ к такому свойству можно откуда угодно:
class Greeter {
public greet() {
console.log('hi!');
}
}
const g = new Greeter();
g.greet();
protected
Защищенные свойства видны только подклассам того класса, в котором они объявлены.
class Greeter {
public greet() {
console.log('Hello, ' + this.getName())
}
protected getName() {
return 'hi';
}
}
class SpecialGreeter extends Greeter {
public howdy() {
// OK. можем получить доступ к защищенному полю
console.log('Howdy, ' + this.getName());
}
}
const g = new SpecialGreeter();
g.greet(); // OK
g.getName(); // Error
private
private
похож на protected
, но не разрешает доступ к свойству даже из подклассов:
class Base {
private x = 0;
}
const b = new Base();
console.log(b.x); // Error
class Derived extends Base {
showX() {
// нет доступа в подклассе
console.log(this.x); // Error
}
}
abstract Classes
Классы, методы и поля в TypeScript могут быть абстрактными. Абстрактный метод или поле — это метод, для которого не предусмотрена реализация. Такие методы должны быть внутри абстрактного класса, который не может быть создан напрямую.
Роль абстрактных классов — служить базовым классом для подклассов, которые реализуют все абстрактные методы и поля.
abstract class Base {
abstract getName(): string;
printName() {
console.log('Hello, ' + this.getName());
}
}
const b = new Base(); // Error. Не можем создать экземпляр абстрактного класса
class Derived extends Base {
getName() {
return 'world';
}
}
const d = new Derived();
d.printName(); // Hello, world
Generics
Дженерики похожи на аргументы функции, только хранят в себе не значения, а типы значений.
На примере функции identity
, которая возвращает тоже, что и принимает, объявлен тип T
, который будет равен типу
переданного аргумента. Мы также можем использовать полученный тип T
, как тип возвращаемого значения функции:
function identity<T>(arg: T): T {
return arg;
}
Теперь, можно вызвать функцию identity
одним из двух способов. Первый способ — передать все аргументы, включая
аргумент типа:
const result = identity<string>('myString');
// ^ = const result: string
Здесь мы явно устанавливаем T
как строку в качестве одного из аргументов вызова функции, обозначенного с помощью <>
вокруг аргументов, в не ()
.
Второй способ — когда используется автоматическое определение типа компилятором в зависимости от типа аргумента, который передан в функцию:
const result = identity('myString');
// ^ = const result: string
Встроенные generics (utilits)
TypeScript предоставляет несколько служебных типов для упрощения работы с общими типами.
Omit
Omit
«выкидывает» из переданного интерфейса определенные ключи, создавая на его основе новый тип:
interface ITodo {
title: string;
description: string;
date: string;
completed: boolean;
}
type TTodoPreview = Omit<ITodo, 'description' | 'date'>
// Тип TTodoPreview не содержит ключи description и date
const todo: TTodoPreview = {
title: 'Clean room',
completed: false,
}
Pick
Pick
работает как Omit
, только наоборот. Он забирает определенные ключи из переданного интерфейса, исключая все
остальные ключи:
interface ITodo {
title: string;
description: string;
date: string;
completed: boolean;
}
type TTodoPreview = Pick<ITodo, 'title' | 'completed'>
// Тип TTodoPreview содержит только ключи title и completed
const todo: TTodoPreview = {
title: 'Clean room',
completed: false,
}
Partial
Partial
принимает только один аргумент (интерфейс), и создает новый тип, делая все ключи интерфейса необязательными.
interface ITodo {
title: string;
description: string;
}
const partialTodo: Partial<ITodo> = {
title: 'hi',
// все свойства не обязательны
}