Типизация функций с помощью TypeScript
год назад·15 мин. на чтение
Туториал по TypeScript - Типизация функций TypeScript
Содержание туториала по TypeScript
Функции — это основной строительный блок любого приложения, будь то локальные функции, импортированные из другого модуля или методы класса. Они также являются значениями, и, как и другие значения, в TypeScript есть много способов описать, как можно вызывать функции.
Определение
С помощью анализа потока кода TypeScript сделает вывод о том, чем является
Специальный тип
Тип
Некоторые функции никогда не возвращают значение:
Глобальный тип
Возвращаемый тип
Возвращаемый тип
Типизация функций
Самый простой способ типизировать функцию — использовать выражение функционального типа. Эти типы синтаксически похожи на стрелочные функции:Синтаксисfunction greeter(fn: (a: string) => void) { fn('Hello, World'); } function printToConsole(s: string) { console.log(s); } greeter(printToConsole);
(a:string) => void
означает "функция с одним параметром a
, типа string
, который не имеет возвращаемого значения". Как и в случае с определением функции, если тип параметра не указан, он будет иметь тип any
.
Обратите внимание, что имя параметра является обязательным. Тип функции (string) => void
означает "функция с параметром, названным string
типа any
"!
Конечно, мы можем использовать псевдоним типа для обозначения типа функции:
type GreetFunction = (a: string) => void; function greeter(fn: GreetFunction) { // ... }
Сигнатура вызова (Call Signature)
В JavaScript функции могут не только вызываться, но и иметь свойства. Однако синтаксис выражения функционального типа не позволяет объявлять свойства. Если мы хотим описать что-то вызываемое с помощью свойств, мы можем написать сигнатуру вызова в объектном типе:Обратите внимание, что синтаксис немного отличается от выражения функционального типа — используетсяtype DescribableFunction = { description: string; (someArg: number): boolean; }; function doSomething(fn: DescribableFunction) { console.log(fn.description + ' returned ' + fn(6)); }
:
между списком параметров и возвращаемым типом, а не =>
.
Сигнатура конструктора (Construct Signature)
Функции JavaScript также можно вызывать с помощью оператораnew
. В TypeScript они считаются конструкторами, потому что они обычно создают новый объект. Вы можете написать сигнатуру конструктора, добавив ключевое слово new
перед сигнатурой вызова:
Некоторые объекты, такие как объектtype SomeConstructor = { new (s: string): SomeObject; }; function fn(ctor: SomeConstructor) { return new ctor('hello'); }
Date
в JavaScript, можно вызывать как с оператором new
, так и без него. Вы можете произвольно комбинировать сигнатуры вызова и конструктора в одном и том же типе:
interface CallOrConstruct { new (s: string): Date; (n?: number): number; }
Функции-дженерики (Generic Functions)
Обычно пишут функцию, в которой типы входных данных связаны с типом выходных данных или где типы двух входных данных каким-то образом связаны. Давайте рассмотрим функцию, которая возвращает первый элемент массива:Эта функция выполняет свою работу, но, к сожалению, имеет возвращаемый типfunction firstElement(arr: any[]) { return arr[0]; }
any
. Лучше бы функция возвращала тип элемента массива.
В TypeScript дженерики используются, когда мы хотим описать соответствие между двумя значениями. Мы делаем это, объявляя параметр типа в сигнатуре функции:
Добавив к этой функции параметрfunction firstElement<Type>(arr: Type[]): Type | undefined { return arr[0]; }
Type
и используя его в двух местах, мы создали связь между входными данными функции (массивом) и выходными (возвращаемым значением). Теперь, когда мы ее вызываем, получается более конкретный тип:
// s имеет тип 'string' const s = firstElement(['a', 'b', 'c']); // n имеет тип 'number' const n = firstElement([1, 2, 3]); // u имеет тип undefined const u = firstElement([]);
Предположение типа (Inference)
Мы можем использовать несколько параметров типа. Например, самописная версия функцииmap
может выглядеть так:
Обратите внимание, что в приведенном примере TypeScript может сделать вывод относительно типаfunction map<Input, Output>( arr: Input[], func: (arg: Input) => Output ): Output[] { return arr.map(func); } // Параметр 'n' имеет тип 'string' // 'parsed' имеет тип 'number[]' const parsed = map(['1', '2', '3'], (n) => parseInt(n));
Input
на основе переданного string[]
, а относительно типа Output
на основе возвращаемого number
.
Ограничения (constraints)
Ограничение используется для того, чтобы ограничивать типы, которые принимаются параметром типа. Реализуем функцию, возвращающую самое длинное из двух значений. Для этого нам потребуется свойствоlength
, которое будет числом. Мы ограничим параметр типа типом number
с помощью ключевого слова extends
:
В этом примере есть несколько интересных моментов. Мы позволили TypeScript определять возвращаемый тип самого длинного значения. Вывод типа возвращаемого значения также работает с функциями-дженериками. Поскольку мы ограничилиfunction longest<Type extends { length: number }>(a: Type, b: Type) { if (a.length >= b.length) { return a; } else { return b; } } // longerArray имеет тип 'number[]' const longerArray = longest([1, 2], [1, 2, 3]); // longerString имеет тип 'alice' | 'bob' const longerString = longest('alice', 'bob'); // Ошибка! У чисел нет свойства 'length' const notOK = longest(10, 100); // Argument of type 'number' is not assignable to parameter of type '{ length: number; }'. // Аргумент типа 'number' не может быть присвоен аргументу типа '{ length: number; }'.
Type
значением {length: number}
, мы смогли получить доступ к свойству .length
параметров a
и b
. Без ограничения типа мы не смогли бы получить доступ к этим свойствам, потому что значения могли быть какого-то другого типа без свойства length
.
Типы longerArray
и longerString
были выведены на основе аргументов. Помните, что дженерики — это связывание двух или более значений с одним и тем же типом.
Наконец, как мы и хотели, вызов longest(10, 100)
,был отклонен, потому что тип number
не имеет свойства .length
.
Работа со значениями с ограничениями
Вот распространенная ошибка при работе с ограничениями-дженериками:Может показаться, что с этой функцией все в порядке —function minimumLength<Type extends { length: number }>( obj: Type, minimum: number ): Type { if (obj.length >= minimum) { return obj; } else { return { length: minimum }; // Type '{ length: number; }' is not assignable to type 'Type'. // '{ length: number; }' is assignable to the constraint of type 'Type', but 'Type' could be instantiated with a different subtype of constraint '{ length: number; }'. } }
Type
ограничен до { length: number }
, и функция либо возвращает Type
, либо значение, соответствующее этому ограничению. Проблема в том, что функция обещает вернуть тот же тип объекта, который был передан, а не просто какой-то объект, соответствующий ограничению. Если бы этот код был работающим, вы могли бы написать код, который не работал бы:
// 'arr' получает значение { length: 6 } const arr = minimumLength([1, 2, 3], 6); // и падает, т.к. массив имеет метод 'slice' // но не возвращаемый объект! console.log(arr.slice(0));
Определение типа аргументов
TypeScript обычно может вывести предполагаемые аргументы типа в вызове дженерика, но не всегда. Например, вы написали функцию для объединения двух массивов:Обычно было бы ошибкой вызывать эту функцию с несовпадающими массивами:function combine<Type>(arr1: Type[], arr2: Type[]): Type[] { return arr1.concat(arr2); }
Однако, если вы намеревались сделать это, вы можете вручную указатьconst arr = combine([1, 2, 3], ['hello']); // Type 'string' is not assignable to type 'number'. // Нельзя присвоить тип 'string' типу 'number'.
Type
:
const arr = combine<string | number>([1, 2, 3], ['hello']);
Как написать хорошую функцию-дженерик?
Написание функций-дженериков — это весело, и можно легко увлечься параметрами типа. Наличие слишком большого количества параметров типа или использование ограничений там, где они не нужны, может сделать вывод менее успешным, вызывая разочарование у пользователей функции.Используйте параметры типа без ограничений
Вот два способа написания функции, которые кажутся похожими:На первый взгляд они могут показаться идентичными, ноfunction firstElement1<Type>(arr: Type[]) { return arr[0]; } function firstElement2<Type extends any[]>(arr: Type) { return arr[0]; } // a: number (хорошо) const a = firstElement1([1, 2, 3]); // b: any (плохо) const b = firstElement2([1, 2, 3]);
firstElement1
— гораздо лучший способ написать эту функцию. Предполагаемый тип возвращаемого значения — Type
, но предполагаемый возвращаемый тип firstElement2
— any
, поскольку TypeScript должен разрешать выражение arr[0]
с использованием типа ограничения, а не «ждать» элемент во время вызова.
Правило: по возможности используйте сам параметр типа, а не ограничивайте его.
Используйте меньше параметров типа
Вот еще пара похожих функций:Мы создали параметр типаfunction filter1<Type>(arr: Type[], func: (arg: Type) => boolean): Type[] { return arr.filter(func); } function filter2<Type, Func extends (arg: Type) => boolean>( arr: Type[], func: Func ): Type[] { return arr.filter(func); }
Func
, который не связывает два значения. Это всегда красный флаг, потому что это означает, что вызывающие программы, желающие указать аргументы типа, должны вручную указать дополнительный аргумент типа без всякой причины. Func
ничего не делает, но затрудняет чтение и осмысление функции!
Правило: всегда используйте как можно меньше параметров типа
Параметры типа должны появляться дважды
Иногда мы забываем, что функции не обязательно быть дженериком:Мы могли бы написать более простую версию:function greet<Str extends string>(s: Str) { console.log('Hello, ' + s); } greet('world');
Помните, что параметры типа предназначены для связи типов нескольких значений. Если параметр типа используется только один раз в сигнатуре функции, он ни с чем не связан. Правило: если параметр типа появляется только в одном месте, серьезно подумайте, действительно ли он вам нужен.function greet(s: string) { console.log('Hello, ' + s); }
Необязательные параметры
Функции в JavaScript часто принимают переменное количество аргументов. Например, методtoFixed
для значений типа number
принимает необязательное количество цифр:
Мы можем смоделировать это в TypeScript, пометив параметр как необязательный с помощьюfunction f(n: number) { console.log(n.toFixed()); // 0 аргументов console.log(n.toFixed(3)); // 1 аргумент }
?
:
Хотя параметр указан как типаfunction f(x?: number) { // ... } f(); // OK f(10); // OK
number
, параметр x
на самом деле будет иметь тип number | undefined
, потому что неуказанные параметры в JavaScript получают значение undefined
.
Вы также можете указать параметр по умолчанию:
Теперь в телеfunction f(x = 10) { // ... }
f
, x
будет иметь тип number
, потому что любой неопределенный аргумент будет заменен на 10
. Обратите внимание, что, когда параметр является необязательным, вызывающая сторона всегда может передать значение undefined
, так как это просто имитирует «отсутствующий» аргумент:
declare function f(x?: number): void; // все вызовы допустимы f(); f(10); f(undefined);
Необязательные параметры в функциях обратного вызова
Мы уже знаем о необязательных параметрах и типизации функциональных выражений. Очень легко сделать следующие ошибки при написании функций, которые вызывают колбеки:Обычно при написанииfunction myForEach(arr: any[], callback: (arg: any, index?: number) => void) { for (let i = 0; i < arr.length; i++) { callback(arr[i], i); } }
index?
в качестве необязательного параметр разработчики хотят, чтобы оба этих вызова валидными:
На самом деле это означает, что колбек может быть вызван с одним аргументом. Другими словами, в определении функции сказано, что реализация может выглядеть так:myForEach([1, 2, 3], (a) => console.log(a)); myForEach([1, 2, 3], (a, i) => console.log(a, i));
В свою очередь, TypeScript будет применять это значение и выдавать ошибки:function myForEach(arr: any[], callback: (arg: any, index?: number) => void) { for (let i = 0; i < arr.length; i++) { callback(arr[i]); } }
В JavaScript, если вы вызываете функцию с бОльшим количеством аргументов, лишние аргументы просто игнорируются. TypeScript ведет себя точно так же. Функции с меньшим количеством параметров (одного и того же типа) всегда могут заменить функции с бОльшим количеством параметров. При типизации функции для колбека никогда делайте параметр необязательным, если вы не собираетесь вызывать функцию без передачи этого аргумента.myForEach([1, 2, 3], (a, i) => { console.log(i.toFixed()); // Object is possibly 'undefined'. // Объект, возможно, 'undefined'. });
Перегрузка функций (Function Overloads)
Некоторые функции JavaScript можно вызывать с различным числом аргументов и типами. Например, вы можете написать функцию для создания датыDate
, которая принимает отметку времени (один аргумент) или спецификацию месяц/день/год (три аргумента).
В TypeScript мы можем указать функцию, которую можно вызывать по-разному, написав сигнатуры перегрузки. Для этого нужно написать несколько сигнатур функции (обычно две или более), а затем тело функции:
В этом примере мы написали две перегрузки: одну, принимающую один аргумент, и другую, принимающую три аргумента. Эти первые две сигнатуры называются сигнатурами перегрузки. Затем мы написали реализацию функции с совместимой сигнатурой. Функции имеют сигнатуру реализации, но эту сигнатуру нельзя вызвать напрямую. Несмотря на то, что мы написали функцию с двумя необязательными параметрами после обязательного, ее нельзя вызвать с двумя параметрами!function makeDate(timestamp: number): Date; function makeDate(m: number, d: number, y: number): Date; function makeDate(mOrTimestamp: number, d?: number, y?: number): Date { if (d !== undefined && y !== undefined) { return new Date(y, mOrTimestamp, d); } else { return new Date(mOrTimestamp); } } const d1 = makeDate(12345678); const d2 = makeDate(5, 5, 5); const d3 = makeDate(1, 3); // No overload expects 2 arguments, but overloads do exist that expect either 1 or 3 arguments. // Нет перегрузки, ожидающей 2 аргумента, но есть перегрузки, которые ожидают либо 1, либо 3 аргумента.
Сигнатуры перегрузки и сигнатура реализации
Это распространенный источник путаницы. Часто люди пишут такой код и не понимают, почему возникает ошибка:Сигнатура, используемая для написания тела функции, не может быть использована извне. Сигнатура реализации не видна снаружи. При написании перегруженной функции вы всегда должны иметь две или более сигнатуры над реализацией функции. Сигнатура реализации также должна быть совместима с сигнатурами перегрузки. Например, в этих функциях есть ошибки, потому что сигнатура реализации не соответствует перегруженным версиям должным образом:function fn(x: string): void; function fn() { // ... } // Expected to be able to call with zero arguments // Ожидается, что можно вызвать без аргументов fn(); // Expected 1 arguments, but got 0. // Ожидается 1 аргумент, но получено 0.
function fn(x: boolean): void; // Неправильный аргумент функции function fn(x: string): void; // This overload signature is not compatible with its implementation signature. // Эта перегрузка сигнатуры не совместима с сигнатурой реализации. function fn(x: boolean) {}
function fn(x: string): string; // Неверный возвращаемый тип function fn(x: number): boolean; // This overload signature is not compatible with its implementation signature. // Cигнатура перегрузки не совместима с сигнатурой реализации. function fn(x: string | number) { return 'oops'; }
Как написать хорошую перегрузку
Как и в случае с дженериками, при использовании перегруженных функций следует соблюдать несколько рекомендаций. Следование этим принципам упростит вызов вашей функции, ее понимание и реализацию. Рассмотрим функцию, которая возвращает длину строки или массива:С этой функцией все в порядке; мы можем вызывать ее со строками или массивами. Однако мы не можем вызвать ее со значением, которое может быть строкой или массивом, потому что TypeScript может разрешить вызов функции только для одной перегрузки:function len(s: string): number; function len(arr: any[]): number; function len(x: any) { return x.length; }
Поскольку обе перегрузки имеют одинаковое количество аргументов и один и тот же тип возвращаемого значения, вместо этого мы можем написать не перегруженную версию функции:len(''); // OK len([0]); // OK len(Math.random() > 0.5 ? 'hello' : [0]); // No overload matches this call. // Overload 1 of 2, '(s: string): number', gave the following error. // Argument of type 'number[] | "hello"' is not assignable to parameter of type 'string'. // Type 'number[]' is not assignable to type 'string'. // Overload 2 of 2, '(arr: any[]): number', gave the following error. // Argument of type 'number[] | "hello"' is not assignable to parameter of type 'any[]'. // Type 'string' is not assignable to type 'any[]'.
Так гораздо лучше! Ее можно вызывать со значением любого типа, и в качестве дополнительного бонуса нам не нужно вычислять правильную сигнатуру реализации. Всегда предпочитайте параметры с объединением вместо перегрузок, когда это возможно.function len(x: any[] | string) { return x.length; }
Определение this
в функциях
С помощью анализа потока кода TypeScript сделает вывод о том, чем является this
:
TypeScript понимает, что функцияconst user = { id: 123, admin: false, becomeAdmin: function () { this.admin = true; }, };
user.becomeAdmin
имеет соответствующий this
, который является объектом user
извне. В спецификации JavaScript указано, что у вас не может быть параметра с именем this
, TypeScript использует это, чтобы можно было объявить тип для this
в теле функции.
Этот шаблон распространен в API обратного вызова, где другой объект обычно управляет вызовом вашей функции. Обратите внимание, что вам нужно использоватьinterface DB { filterUsers(filter: (this: User) => boolean): User[]; } const db = getDB(); const admins = db.filterUsers(function (this: User) { return this.admin; });
function
, а не стрелочные функции, чтобы получить такое поведение:
interface DB { filterUsers(filter: (this: User) => boolean): User[]; } const db = getDB(); const admins = db.filterUsers(() => this.admin); // The containing arrow function captures the global value of 'this'. // Стрелочная функция захватывает глобальный `this`. // Element implicitly has an 'any' type because type 'typeof globalThis' has no index signature. // Элемент неявно имеет тип 'any' т.к. тип 'typeof globalThis' не имеет сигнатуры.
Другие типы, о которых следует знать
Есть несколько дополнительных типов, которые часто появляются при работе с типами функций. Как и все типы, вы можете использовать их везде, но они особенно актуальны в функциях.void
void
представляет возвращаемое значение функций, которые не возвращают значения. Этот тип выведется из функции, когда функция не имеет операторов return
или не возвращает никакого явного значения из этих операторов return
:
В JavaScript функция, которая не возвращает никакого значения, неявно вернет значение// Выведенный тип возвращаемого результата void function noop() { return; }
undefined
. Однако void
и undefined
— это не одно и то же в TypeScript. Дополнительные подробности приведены в конце этой главы.
object
Специальный тип object
относится к любому значению, не являющемуся примитивом (string
, number
, bigint
, boolean
, symbol
, null
или undefined
). Это отличается от типа пустого объекта { }
, а также отличается от глобального типа Object
. Очень вероятно, что вы никогда не будете использовать Object
.
object
не является Object
. Всегда используйте object
!
Обратите внимание, что в JavaScript функции являются объектами: у них есть свойства, есть Object.prototype
в своей цепочке прототипов, являются instanceof Object
, вы можете вызывать для них Object.keys
и т.д. По этой причине типы функций считаются object
в TypeScript.
unknown
Тип unknown
представляет любое значение. Это похоже на тип any
, но безопаснее, потому что нельзя ничего делать с неизвестным значением:
Это полезно при описании типов функций, потому что вы можете описывать функции, которые принимают любое значение, не имея значенийfunction f1(a: any) { a.b(); // OK } function f2(a: unknown) { a.b(); // Object is of type 'unknown'. // Объект типа 'unknown'. }
any
в теле вашей функции.
И наоборот, вы можете описать функцию, которая возвращает значение типа unknown
:
function safeParse(s: string): unknown { return JSON.parse(s); } // Нужно быть осторожным с 'obj'! const obj = safeParse(someRandomString);
never
Некоторые функции никогда не возвращают значение:
Типfunction fail(msg: string): never { throw new Error(msg); }
never
представляет значения, которые никогда не возвращаются. В возвращаемом типе это означает, что функция выдает исключение или завершает выполнение программы.
never
появляется, когда TypeScript определяет, что в объединении ничего не осталось.
function fn(x: string | number) { if (typeof x === 'string') { // что-то делаем } else if (typeof x === 'number') { // что-то делаем еще } else { x; // имеет тип 'never'! } }
Function
Глобальный тип Function
описывает такие свойства, как bind
, call
, apply
и другие, присутствующие во всех значениях функций в JavaScript. Он также имеет специальное свойство, позволяющее вызывать значения типа Function — такие вызовы возвращают any:
Это нетипизированный вызов функции, и его обычно лучше избегать из-за небезопасного возвращаемого типаfunction doSomething(f: Function) { return f(1, 2, 3); }
any
.
Если вам нужно принять произвольную функцию без ее вызова, тип () => void
, как правило, безопаснее.
Остальные параметры и аргументы (rest)
Остальные параметры
В дополнение к использованию необязательных параметров или перегрузок функций, которые могут принимать множество фиксированных аргументов, мы также можем определить функции, которые принимают неограниченное количество аргументов, используя синтаксис остальных параметров (rest parameters). Остальные параметры появляется после всех остальных параметров и используют синтаксис...
:
В TypeScript аннотация типа для этих параметров неявно являетсяfunction multiply(n: number, ...m: number[]) { return m.map((x) => n * x); } // 'a' имеет значение [10, 20, 30, 40] const a = multiply(10, 1, 2, 3, 4);
any[]
вместо any
, и любая указанная аннотация типа должна иметь форму Array<T>
или T[]
или тип кортежа (о котором мы узнаем позже).
Остальные аргументы
И наоборот, мы можем предоставить переменное количество аргументов из массива, используя синтаксис распыления (spread syntax). Например, метод массивовpush
принимает любое количество аргументов:
Обратите внимание, что в целом TypeScript не предполагает, что массивы иммутабельные. Это может привести к неожиданному поведению:const arr1 = [1, 2, 3]; const arr2 = [4, 5, 6]; arr1.push(...arr2);
Лучшее решение для этой ситуации зависит от вашего кода, но в целом// Предполагаемый тип number[] - массив с двумя или более числами, // не конкретно с двумя числами const args = [8, 5]; const angle = Math.atan2(...args); // A spread argument must either have a tuple type or be passed to a rest parameter. // Распыленный аргумент должен быть типом кортежа или отправлен как остальные параметры (rest)
const
является наиболее простым решением:
Использование остальных аргументов может потребовать включения// Представлен как кортеж длины 2 const args = [8, 5] as const; // OK const angle = Math.atan2(...args);
downlevelIteration
если старые среды выполнения являются целевыми.
Деструктуризация параметров (Parameter Destructuring)
Вы можете использовать деструктуризацию параметров для удобной распаковки объектов, предоставленных в качестве аргумента, в одну или несколько локальных переменных в теле функции. В JavaScript это выглядит так:Аннотация типа для объекта идет после синтаксиса деструктурирования:function sum({ a, b, c }) { console.log(a + b + c); } sum({ a: 10, b: 3, c: 9 });
Это может выглядеть немного многословно, но здесь вы также можете использовать именованный тип:function sum({ a, b, c }: { a: number; b: number; c: number }) { console.log(a + b + c); }
type ABC = { a: number; b: number; c: number }; function sum({ a, b, c }: ABC) { console.log(a + b + c); }
Присваиваемость функций
Возвращаемый тип void
Возвращаемый тип void
для функций может привести к необычному, но ожидаемому поведению.
Контекстуальная типизация (contextual typing) с возвращаемым типом void
не заставляет функции ничего не возвращать. Иными словами, По-другому можно сказать, что функция с возвращаемым типом void
(type vf = () => void
), при реализации может вернуть любое другое значение, но оно будет проигнорировано.
Таким образом, допустимы следующие реализации () => void
:
И когда возвращаемое значение одной из этих функций будет присвоено другой переменной, оно сохранит типtype voidFunc = () => void; const f1: voidFunc = () => { return true; }; const f2: voidFunc = () => true; const f3: voidFunc = function () { return true; };
void
:
Поэтому следующий код валидный, несмотря на то, чтоconst v1 = f1(); const v2 = f2(); const v3 = f3();
Array.prototype.push
возвращает number
, а метод Array.prototype.forEach
ожидает функцию с возвращаемым типом void
.
Есть еще один особый случай, о котором следует знать, когда литеральное определение функции имеет возвращаемый типconst src = [1, 2, 3]; const dst = [0]; src.forEach((el) => dist.push(el));
void
, эта функция не должна ничего возвращать.
function f2(): void { // @ts-expect-error return true; } const f3 = function (): void { // @ts-expect-error return true; };
Основы TypeScript
год назад·11 мин. на чтение
Туториал по TypeScript - Основы
Содержание туториала по TypeScript
Каждое значение в JavaScript имеет определенное поведение, которое можно наблюдать при выполнении различных операций. Звучит абстрактно, но в качестве быстрого примера рассмотрим некоторые операции, которые мы можем выполнять над переменной с именем
Мы говорили о проверке типов, но мы еще не использовали его на практике. Давайте познакомимся с новым функционалом - это
Напомним, что в некоторых местах TypeScript не пытается выводить за нас типы, а вместо этого возвращается к наиболее мягкому типу:
По умолчанию такие значения, как
message
.
Разберем этот код. Первая исполняемая строка кода обращается к свойству с именем// Получение свойства 'toLowerCase' переменной 'message' и его вызов message.toLowerCase(); // Вызов 'message' message();
toLowerCase
, а затем вызывает его. Вторая пытается вызвать message
напрямую.
Но предполагая, что мы не знаем значения message
— а это довольно частая ситуация — мы не можем достоверно сказать, какие результаты мы получим, пытаясь запустить любой из этих вариантов. Поведение каждой операции полностью зависит от того, какое значение message
мы имели ввиду.
- Можно ли вызвать
message
? - Есть ли у него свойство
toLowerCase
? - Если это так, можно ли вызвать
toLowerCase
? - Если оба эти значения можно вызвать, что они возвращают?
message
было определено следующим образом.
Как вы, наверное, догадались, если мы попытаемся запуститьconst message = 'Hello World!';
message.toLowerCase()
, мы получим ту же строку, только в нижнем регистре.
А как насчет второй строки кода? Если вы знакомы с JavaScript, вы знаете, что это вызовет исключение:
TypeError: message is not a function
Было бы здорово, если бы мы могли избежать подобных ошибок.
Когда мы запускаем наш код, среда выполнения JavaScript выбирает, что делать, выясняя тип значения — каким поведение оно обладает и какие возможности у него есть. Это часть того, на что намекает TypeError
— это говорит о том, что строку Hello World!
нельзя вызывать как функцию.
Для некоторых значений, таких как примитивы string
и number
, мы можем определить их тип во время выполнения с помощью оператора typeof
. Но для других вещей, таких как функции, нет соответствующего механизма времени выполнения для определения их типов. Например, рассмотрим эту функцию:
Читая код, мы можем заметить, что эта функция будет работать только в том случае, если задан объект с вызываемым свойствомfunction fn(x) { return x.flip(); }
flip
, но JavaScript не отображает эту информацию таким образом, чтобы мы могли проверить ее во время выполнения кода. Единственный способ в чистом JavaScript узнать, что делает fn
с конкретным значением — это вызвать его и посмотреть, что произойдет. Такое поведение затрудняет прогнозирование того, что будет делать код до его запуска, а это означает, что сложнее узнать, что будет делать ваш код, пока вы его пишете.
С этой точки зрения тип — это понятие, описывающее, какие значения могут быть переданы в fn
, а какие приведут к ошибке. JavaScript действительно обеспечивает только динамическую типизацию — нужно запустить код, чтобы увидеть, что происходит.
Альтернативой является использование статической системы типов для предсказания того, какой код ожидается до его запуска.
Статическая проверка типов
Вспомните об ошибкеTypeError
, которую мы получили ранее при попытке вызвать строку как функцию. Большинству людей не нравится получать какие-либо ошибки при выполнении своего кода — это считается багами! И когда мы пишем новый код, мы делаем все возможное, чтобы избежать появления новых багов.
Если мы добавим немного кода, сохраним наш файл, повторно запустим код и сразу же увидим ошибку, мы сможем быстро изолировать проблему; но это не всегда так. Возможно, мы недостаточно тщательно протестировали эту функцию, поэтому мы никогда не столкнемся с потенциальной ошибкой, которая будет выдана! Или, если бы нам посчастливилось стать свидетелями ошибки, мы могли бы в конечном итоге провести масштабный рефакторинг и добавить много другого кода, в котором нам пришлось бы разбираться.
В идеале у нас мог бы быть инструмент, который помогал бы нам находить эти ошибки до запуска нашего кода. Это то, что делает статическая проверка типов, такая как TypeScript. Системы статических типов описывают формы и поведение наших значений при запуске наших программ. Средство проверки типов, такое как TypeScript, использует эту информацию и сообщает нам, когда что-то может пойти не так.
Запуск этого последнего примера с помощью TypeScript даст нам сообщение об ошибке, прежде чем мы запустим код.const message = "hello!"; message(); This expression is not callable. Type 'String' has no call signatures.
Ошибки не вызывающие исключений
До сих пор мы обсуждали некоторые вещи, такие как ошибки времени выполнения — случаи, когда среда выполнения JavaScript сообщает нам, что считает что-то бессмысленным. Такие случаи возникают из-за того, что в спецификации ECMAScript есть четкие инструкции о том, как язык должен вести себя, когда он сталкивается с чем-то непредвиденным. Например, в спецификации сказано, что попытка вызвать что-то, что нельзя вызвать, должна выбросить ошибку. Возможно, это звучит как «очевидное поведение», но вы можете себе представить, что доступ к свойству, не существующему в объекте, также должен вызывать ошибку. Вместо этого JavaScript дает нам другое поведение и возвращает значениеundefined
:
В конечном счете, статическая система типов должна решить, какой код следует пометить как ошибку, даже если это валидный JavaScript, который не выдаст ошибку сразу. В TypeScript следующий код выдает ошибку о том, чтоconst user = { name: 'Daniel', age: 26, }; user.location; // возвращает undefined
location
не определен:
Хотя иногда это подразумевает компромисс в том, что вы можете выразить, цель состоит в том, чтобы поймать законные ошибки в наших программах. И TypeScript отлавливает множество таких ошибок. Например: опечатки,const user = { name: 'Daniel', age: 26, }; user.location; // Property 'location' does not exist on type '{ name: string; age: number; }'.
невызванные функции,const announcement = 'Hello World!'; // Как быстро вы можете заметить опечатку? announcement.toLocaleLowercase(); announcement.toLocalLowerCase(); // Возможно, мы имели ввиду это... announcement.toLocaleLowerCase();
или простые логические ошибки.function flipCoin() { // Имелось ввиду Math.random() return Math.random < 0.5; // Оператор '<' не может применяться к типам '() => number' и 'number'. }
const value = Math.random() < 0.5 ? 'a' : 'b'; if (value !== 'a') { // ... } else if (value === 'b') { // Это условие всегда будет возвращать 'false', т.к. типы '"a"' и '"b"' не пересекаются. // Не достижимый код }
IDE инструменты TypeScript
TypeScript может обнаруживать баги, когда мы делаем ошибки в нашем коде. Это здорово, но TypeScript также может уберечь нас от этих ошибок. У средства проверки типов есть информация для проверки, например, обращаемся ли мы к нужным свойствам переменных и других свойств. Получив эту информацию, он также может начать предлагать свойства, которые вы, возможно, захотите использовать. Это означает, что TypeScript можно использовать и для редактирования кода, а ядро проверки типов может предоставлять сообщения об ошибках и автодополнять кода по мере ввода в редакторе. Это часть того, что люди часто имеют в виду, когда говорят об инструментах в TypeScript.tsc
компилятор TypeScript
Мы говорили о проверке типов, но мы еще не использовали его на практике. Давайте познакомимся с новым функционалом - это tsc
- компилятором TypeScript. Сначала нам нужно установить его через npm.
Эта команда устанавливает tsc - компилятор TypeScript - глобально. Вы можете использовать npx или аналогичные инструменты, если предпочитаете запускать tsc из локального node_modules. Теперь давайте перейдем в пустую папку и попробуем написать нашу первую программу на TypeScript:npm install -g typescript
hello.ts
:
Обратите внимание, что здесь нет никаких дополнений; эта программа «hello world» выглядит также, как написанная на JavaScript. А теперь давайте проверим эту программы на типы, выполнив команду tsc, которая была установлена пакетом typescript.// Приветствует мир console.log('Hello world!');
Что произошло при вызове этого кода. Мы запустили tsc и ничего не произошло! Ошибок типов не было, поэтому мы не получили никакого сообщения в консоли, поскольку сообщать было не о чем. Но проверьте еще раз — вместо этого мы получили какой-то файл. Если мы посмотрим в текущий каталог, то увидим файлtsc hello.ts
hello.js
рядом с hello.ts
. Это результат полученный из файла hello.ts
после того, как tsc скомпилировал (или преобразовал) его в обычный файл JavaScript. И если мы проверим содержимое, то увидим, что выдал TypeScript после обработки файла .ts
:
В этом случае очень мало того, что может преобразовать TypeScript, поэтому код выглядит идентично тому, что мы написали. Компилятор пытается создать чистый читаемый код, похожий на то, что написал бы человек. Хотя это не всегда так просто, TypeScript постоянно делает отступы, учитывает, когда наш код включает разные строки кода, и старается сохранять комментарии. А если бы мы добавили ошибку проверки типов? Давайте перепишем// Приветствует мир console.log('Hello world!');
hello.ts
:
Если мы снова запустимfunction greet(person, date) { console.log(`Hello ${person}, today is ${date}!`); } greet('Brendan');
tsc hello.ts
, мы получим ошибку в командной строке.
TypeScript сообщает нам, что мы забыли передать аргумент функцииExpected 2 arguments, but got 1.
greet
, и это правильно. До сих пор мы писали только стандартный JavaScript, и тем не менее проверка типов по-прежнему могла находить проблемы в нашем коде.
Компиляция с отображением ошибок
В последнем примере вы могли не заметить, что наш файлhello.js
снова изменился. Если мы откроем этот файл, мы увидим, что содержимое по-прежнему в основном выглядит так же, как наш входной файл. Это может показаться немного удивительным, учитывая тот факт, что tsc
сообщил об ошибке в нашем коде, но это основано на одной из основных ценностей TypeScript: большую часть времени вы будете знать лучше, чем TypeScript.
Повторим сказанное ранее: код проверки типов ограничивает количество программ, которые вы можете запускать, поэтому существует компромисс между тем, какие вещи программа проверки типов считает приемлемыми. В большинстве случаев это нормально, но бывают ситуации, когда эти проверки мешают. Например, представьте, что вы переносите код JavaScript на TypeScript и вносите ошибки проверки типов. В конце концов вы научитесь наводить порядок для системы проверки типов, но исходный код и так был JavaScript рабочим. Почему преобразование его в TypeScript должно помешать вам запустить его?
Так что TypeScript не мешает вам. Конечно, со временем вы, возможно, захотите стать немного более защищенным от ошибок и заставить TypeScript действовать более строго. В этом случае вы можете использовать параметр компилятора noEmitOnError
. Попробуйте изменить файл hello.ts
и запустить tsc
с этим флагом:
Вы заметите, чтоtsc --noEmitOnError hello.ts
hello.js
больше не обновляется.
Явные типы
До сих пор мы не сообщали TypeScript, что такоеperson
или date
. Давайте отредактируем код, чтобы сообщить TypeScript, что person
— это строка, а date
должна быть объектом Date
. Мы также будем использовать метод toDateString()
для даты.
Что мы сделали, так это добавили аннотации типа дляfunction greet(person: string, date: Date) { console.log(`Hello ${person}, today is ${date.toDateString()}!`); }
person
и date
, чтобы описать, с какими типами значений может быть вызвана функция greet
. Вы можете прочитать эту сигнатуру как "greet
принимает person
типа string
и date
типа Date
».
Таким образом, TypeScript может рассказать нам о других случаях, когда greet
могло быть вызвано неправильно. Например...
Хм? TypeScript сообщил об ошибке во втором аргументе, но почему? Удивительно, но вызовfunction greet(person: string, date: Date) { console.log(`Hello ${person}, today is ${date.toDateString()}!`); } greet('Maddison', Date()); // Argument of type 'string' is not assignable to parameter of type 'Date'.
Date()
в JavaScript возвращает строку. С другой стороны, создание Date
с помощью new Date()
фактически дает нам то, что мы ожидали.
В любом случае, мы можем быстро исправить ошибку:
Имейте в виду, что нам не всегда нужно писать явные аннотации типов. Во многих случаях TypeScript может даже просто вывести (или «вычислить») типы за нас, даже если мы их опустим.function greet(person: string, date: Date) { console.log(`Hello ${person}, today is ${date.toDateString()}!`); } greet('Maddison', new Date());
Если вы наведете курсор на переменнуюlet msg = 'hello there!';
msg
в IDE, появится всплывающее сообщение с информацией о типе.
Несмотря на то, что мы не сказали TypeScript, что тип msg
это string
, он смог это понять. Это фича, и лучше не добавлять аннотации, когда система типов в любом случае выведет этот тип.
Удаление типов
Давайте посмотрим, что произойдет, когда мы скомпилируем приведенную выше функциюgreeting
с помощью tsc
для вывода JavaScript:
Обратите внимание на две вещи:'use strict'; function greet(person, date) { console.log( 'Hello '.concat(person, ', today is ').concat(date.toDateString(), '!') ); } greet('Maddison', new Date());
- Параметры
person
иdate
больше не имеют аннотаций типа. - Наш шаблонный литерал — та строка, в которой использовались обратные кавычки (символ ```), — была преобразована в простые строки с конкатенацией.
Понижение версии
Еще одно отличие заключалось в том, что наша шаблонная строка была переписана изв`Hello ${person}, today is ${date.toDateString()}!`;
Почему это случилось? Шаблонный литерал — это фича версии ECMAScript под названием ECMAScript 2015 (он же ECMAScript 6, ES2015, ES6 и т.д.). TypeScript имеет возможность переписывать код с более новых версий ECMAScript на более старые, такие как ECMAScript 3 или ECMAScript 5 (также известные как ES3 и ES5). Этот процесс перехода от более новой или «более высокой» версии ECMAScript к более старой или «более низкой» версии иногда называют понижением уровня. По умолчанию TypeScript выводит ES3, чрезвычайно старую версию ECMAScript. Мы могли бы выбрать что-то более свежее, используя опцию'Hello ' + person + ', today is ' + date.toDateString() + '!';
target
. Запуск с --target es2015
изменяет TypeScript на целевой ECMAScript 2015, что означает, что код должен работать везде, где поддерживается ECMAScript 2015. Таким образом, запуск tsc --target es2015 hello.ts
дает нам следующий результат:
function greet(person, date) { console.log(`Hello ${person}, today is ${date.toDateString()}!`); } greet('Maddison', new Date());
Строгость
Разные пользователи приходят в TypeScript в поисках разных вещей в системе проверки типов. Некоторым людям нужно больше необязательности в применении TypeScript, которая может помочь проверить только некоторые части их программы и при этом иметь приличный инструментарий. Это стандартный опыт работы с TypeScript, где типы являются необязательными, для вывода используются самые мягкие типы, и нет проверки значений на потенциальныеnull
/undefined
. Подобно тому, как tsc
выдает сообщения об ошибках, эти значения по умолчанию устанавливаются, чтобы не мешать вам. Если вы переносите существующий JavaScript, это может быть желательным первым шагом.
Напротив, многие пользователи предпочитают, чтобы TypeScript проверял как можно больше сразу, и поэтому язык также предоставляет настройки строгости. Эти настройки строгости превращают статическую проверку типов из переключателя (независимо от того, проверяется ваш код или нет) во что-то более близкое к шкале. Чем больше вы поворачиваете этот переключатель, тем больше TypeScript будет проверять. Это может потребовать небольшой дополнительной работы, но в целом это окупается в долгосрочной перспективе и позволяет проводить более тщательные проверки и использовать более точные инструменты. Когда это возможно, новая кодовая база всегда должна включать эти проверки строгости.
TypeScript имеет несколько флагов строгости проверки типов, которые можно включать и выключать, и все наши примеры будут написаны со всеми этими флагами, если не указано иное. Флаг strict
в CLI "strict": true
в tsconfig.json
включает их все одновременно, но мы можем отказаться от них по отдельности. Вам следует знать о двух наиболее важных из них: noImplicitAny
и strictNullChecks
.
noImplicitAny
Напомним, что в некоторых местах TypeScript не пытается выводить за нас типы, а вместо этого возвращается к наиболее мягкому типу: any
. Это не самое худшее, что может случиться — в конце концов, откат к any
— это в любом случае обычный опыт работы с JavaScript.
Однако использование any
часто противоречит цели использования TypeScript. Чем более типизирована ваша программа, тем больше проверок и инструментов вы получите, а это означает, что вы столкнетесь с меньшим количеством ошибок при написании кода. Включение флага noImplicitAny
вызовет ошибку для любых переменных, тип которых неявно выводится как any
.
strictNullChecks
По умолчанию такие значения, как null
и undefined
, могут быть присвоены любому другому типу. Это может облегчить написание кода, но забывание обрабатывать null
и undefined
является причиной бесчисленных ошибок — некоторые считают это ошибкой на миллиард долларов! Флаг strictNullChecks
делает обработку null
и undefined
более явной и избавляет нас от беспокойства о том, не забыли ли мы обработать null
и undefined
.