TypeScript для функциональных программистов
TypeScript начал свою жизнь как попытка перенести традиционные объектно-ориентированные типы в JavaScript, чтобы программисты Microsoft могли перенести традиционные объектно-ориентированные программы в веб. По мере развития система типов TypeScript эволюционировала, чтобы моделировать код, написанный разработчиками JavaScript. Результирующая система мощная, интересная и запутанная.
Это введение предназначено для работающих программистов Haskell или ML, которые хотят изучить TypeScript. Оно описывает, как система типов TypeScript отличается от системы типов Haskell. Оно также описывает уникальные особенности системы типов TypeScript, возникающие из моделирования кода JavaScript.
В этом введении не рассматривается объектно-ориентированное программирование. На практике объектно-ориентированные программы в TypeScript похожи на те, что в других популярных языках с ОО-возможностями.
Предварительные требования
В этом введении я предполагаю, что вы знаете следующее:
- Как программировать на JavaScript, хорошие части.
- Синтаксис типов языка, происходящего от C.
Если вам нужно изучить хорошие части JavaScript, прочитайте JavaScript: The Good Parts. Возможно, вы сможете пропустить книгу, если знаете, как писать программы на языке с вычислением по значению, лексическим охватом, множеством изменяемости и практически без чего-либо ещё. R4RS Scheme — хороший пример.
The C++ Programming Language — хорошее место, чтобы узнать о синтаксисе типов в стиле C. В отличие от C++, TypeScript использует постфиксные типы, например: x: string вместо string x.
Концепции, отсутствующие в Haskell
Встроенные типы
JavaScript определяет 8 встроенных типов:
| Тип | Описание |
|---|---|
Number | число с двойной точностью IEEE 754 с плавающей запятой. |
String | неизменяемая строка UTF-16. |
BigInt | целые числа в формате произвольной точности. |
Boolean |
true и false. |
Symbol | уникальное значение, обычно используемое в качестве ключа. |
Null | эквивалент типу единицы. |
Undefined | также эквивалент типу единицы. |
Object | аналогично записям. |
См. страницу MDN для получения более подробной информации.
TypeScript имеет соответствующие примитивные типы для встроенных типов:
numberstringbigintbooleansymbolnullundefinedobject
Другие важные типы TypeScript
| Тип | Описание |
|---|---|
unknown | тип-вершина. |
never | тип-дно. |
| объектная запись | например, { property: Type }
|
void | для функций без документированного возвращаемого значения |
T[] | изменяемые массивы, также записываются Array<T>
|
[T, T] | кортежи, которые имеют фиксированную длину, но изменяемы |
(t: T) => U | функции |
Примечания:
-
Синтаксис функции включает имена параметров. К этому довольно трудно привыкнуть!
let fst: (a: any, b: any) => any = (a, b) => a; // or more precisely: let fst: <T, U>(a: T, b: U) => T = (a, b) => a;
-
Синтаксис типов объектных записей тесно отражает синтаксис значений объектных записей:
let o: { n: number; xs: object[] } = { n: 1, xs: [] }; -
[T, T]является подтипомT[]. Это отличается от Haskell, где кортежи не связаны со списками.
Упакованные типы
JavaScript имеет упакованные эквиваленты примитивных типов, которые содержат методы, которые программисты связывают с этими типами. TypeScript отражает это, например, разницей между примитивным типом number и упакованным типом Number. Упакованные типы редко используются, так как их методы возвращают примитивы.
(1).toExponential(); // equivalent to Number.prototype.toExponential.call(1);
Обратите внимание, что вызов метода на числовой литерале требует его заключения в скобки для помощи анализатору.
Постепенная типизация
TypeScript использует тип any всякий раз, когда не может определить тип выражения. По сравнению с Dynamic, называть any типом — это преувеличение. Он просто отключает проверку типов везде, где появляется. Например, вы можете добавить любое значение в any[] без каких-либо пометок для значения:
// with "noImplicitAny": false in tsconfig.json, anys: any[]
const anys = [];
anys.push(1);
anys.push("oh no");
anys.push({ anything: "goes" }); И вы можете использовать выражение типа any где угодно:
anys.map(anys[1]); // oh no, "oh no" is not a function
any также передается по цепочке — если вы инициализируете переменную выражением типа any, переменная также имеет тип any.
let sepsis = anys[0] + anys[1]; // this could mean anything
Чтобы получить ошибку, когда TypeScript создаёт any, используйте "noImplicitAny": true, или "strict": true в tsconfig.json.
Структурная типизация
Структурная типизация знакома большинству функциональных программистов, хотя Haskell и большинство языков ML не являются структурно типизированными. Её базовая форма довольно проста:
// @strict: false
let o = { x: "hi", extra: 1 }; // ok
let o2: { x: string } = o; // ok Здесь объектная запись { x: "hi", extra: 1 } имеет соответствующий литеральный тип { x: string, extra: number }. Этот тип присваивается { x: string } , так как он имеет все необходимые свойства, а эти свойства имеют присваиваемые типы. Дополнительное свойство не препятствует присвоению, оно просто делает его подтипом { x: string }.
Именованные типы просто дают имя типу; для целей присвоения нет разницы между алиасом типа One и типом интерфейса Two ниже. Оба имеют свойство p: string. (Однако алиасы типов ведут себя по-другому, чем интерфейсы, в отношении рекурсивных определений и параметров типов.)
type One = { p: string };
interface Two {
p: string;
}
class Three {
p = "Hello";
}
let x: One = { p: "hi" };
let two: Two = x;
two = new Three(); Объединения
В TypeScript объединения типов не имеют тегов. Другими словами, они не являются объединениями с различением типов, как data в Haskell. Однако вы часто можете различать типы в объединении, используя встроенные теги или другие свойства.
function start(
arg: string | string[] | (() => string) | { s: string }
): string {
// this is super common in JavaScript
if (typeof arg === "string") {
return commonCase(arg);
} else if (Array.isArray(arg)) {
return arg.map(commonCase).join(",");
} else if (typeof arg === "function") {
return commonCase(arg());
} else {
return commonCase(arg.s);
}
function commonCase(s: string): string {
// finally, just convert a string to another string
return s;
}
} string, Array и Function имеют встроенные предикат для типов, удобно оставляя тип объекта для else ветви. Тем не менее, возможно сгенерировать объединения, которые сложно отличить во время выполнения. Для нового кода лучше строить только объединения с различением типов.
Следующие типы имеют встроенные предикаты:
| Тип | Предикат |
|---|---|
| строка | typeof s === "string" |
| число | typeof n === "number" |
| bigint | typeof m === "bigint" |
| булево | typeof b === "boolean" |
| символ | typeof g === "symbol" |
| undefined | typeof undefined === "undefined" |
| функция | typeof f === "function" |
| массив | Array.isArray(a) |
| объект | typeof o === "object" |
Обратите внимание, что функции и массивы являются объектами во время выполнения, но имеют свои собственные предикаты.
Пересечения
Помимо объединений, TypeScript также имеет пересечения:
type Combined = { a: number } & { b: string };
type Conflicting = { a: number } & { a: string }; Combined имеет два свойства, a и b, точно так же, как если бы они были написаны как одна объектная запись. Пересечение и объединение рекурсивны в случае конфликтов, поэтому Conflicting.a: number & string.
Типы единицы
Типы единицы являются подтипами примитивных типов, которые содержат ровно одно примитивное значение. Например, строка "foo" имеет тип "foo" . Поскольку JavaScript не имеет встроенных перечислений, часто используется набор известных строк. Объединения строковых литеральных типов позволяют TypeScript типизировать этот шаблон:
declare function pad(s: string, n: number, direction: "left" | "right"): string;
pad("hi", 10, "left"); Когда это необходимо, компилятор расширяет — преобразует в супертип — тип единицы в примитивный тип, такой как "foo" в string . Это происходит при использовании изменяемости, что может затруднить некоторые применения изменяемых переменных:
let s = "right";
pad("hi", 10, s); // error: 'string' is not assignable to '"left" | "right"' Вот как возникает ошибка:
"right": "right"-
s: string, потому что"right"расширяется доstringпри присвоении изменяемой переменной. -
stringне присваивается"left" | "right"
Вы можете обойти это с помощью аннотации типа для s, но это, в свою очередь, предотвращает присвоения s переменных, которые не являются типа "left" | "right".
let s: "left" | "right" = "right";
pad("hi", 10, s); Концепции, аналогичные Haskell
Контекстная типизация
TypeScript имеет очевидные места, где он может выводить типы, такие как объявления переменных:
let s = "I'm a string!";
Но он также выводит типы в нескольких других местах, которых вы, возможно, не ожидали, если работали с другими языками на основе синтаксиса C:
declare function map<T, U>(f: (t: T) => U, ts: T[]): U[]; let sns = map((n) => n.toString(), [1, 2, 3]);
Здесь n: number в этом примере также, несмотря на то, что T и U не были выведены до вызова. На самом деле, после того как [1,2,3] был использован для вывода T=number, тип возврата n => n.toString() используется для вывода U=string, что приводит к тому, что sns имеет тип string[].
Обратите внимание, что вывод может работать в любом порядке, но IntelliSense будет работать слева направо, поэтому TypeScript предпочитает объявлять map с массивом первым:
declare function map<T, U>(ts: T[], f: (t: T) => U): U[];
Контекстная типизация также работает рекурсивно через объектные записи и для типов единицы, которые в противном случае выводились бы как string или number. И он может выводить типы возвращаемых значений из контекста:
declare function run<T>(thunk: (t: T) => void): T;
let i: { inference: string } = run((o) => {
o.inference = "INSERT STATE HERE";
}); Тип o определяется как { inference: string } , потому что
- Инициализаторы объявления контекстно типизируются типом объявления:
{ inference: string }. - Тип возвращаемого значения вызова использует контекстный тип для вывода, поэтому компилятор выводит, что
T={ inference: string }. - Стрелочные функции используют контекстный тип для типизации своих параметров, поэтому компилятор задаёт
o: { inference: string }.
И происходит это в процессе ввода, так что после ввода o., вы получаете автодополнение для свойства inference, а также для любых других свойств, которые были бы в реальной программе. В целом, эта функция может сделать вывод типов TypeScript похожим на унифицированный движок вывода типов, но это не так.
Псевдонимы типов
Псевдонимы типов — это всего лишь псевдонимы, как type в Haskell. Компилятор попытается использовать имя псевдонима там, где оно использовалось в исходном коде, но не всегда преуспевает в этом.
type Size = [number, number]; let x: Size = [101.1, 999.9];
Наиболее близким эквивалентом newtype является меченный пересечённый тип:
type FString = string & { __compileTimeOnly: any }; Строка FString — это такая же обычная строка, за исключением того, что компилятор думает, что у неё есть свойство с именем __compileTimeOnly, которого на самом деле нет. Это означает, что FString всё ещё можно присвоить string, но не наоборот.
Дискриминированные союзы
Наиболее близким эквивалентом data является объединение типов со свойствами-дискриминаторами, обычно называемыми дискриминированными союзами в TypeScript:
type Shape =
| { kind: "circle"; radius: number }
| { kind: "square"; x: number }
| { kind: "triangle"; x: number; y: number }; В отличие от Haskell, метка или дискриминатор — это всего лишь свойство в каждом типе объекта. Каждый вариант имеет идентичное свойство с разным типом единицы. Это всё ещё обычный объединяемый тип; ведущая | — это необязательная часть синтаксиса объединяемого типа. Вы можете различать члены объединения, используя обычный JavaScript-код:
type Shape =
| { kind: "circle"; radius: number }
| { kind: "square"; x: number }
| { kind: "triangle"; x: number; y: number };
function area(s: Shape) {
if (s.kind === "circle") {
return Math.PI * s.radius * s.radius;
} else if (s.kind === "square") {
return s.x * s.x;
} else {
return (s.x * s.y) / 2;
}
} Обратите внимание, что тип возвращаемого значения area выводится как number, потому что TypeScript знает, что функция полная. Если какой-либо вариант не покрыт, тип возвращаемого значения area будет number | undefined вместо этого.
Кроме того, в отличие от Haskell, общие свойства появляются в любом объединении, поэтому вы можете с пользой различать несколько членов объединения:
function height(s: Shape) {
if (s.kind === "circle") {
return 2 * s.radius;
} else {
// s.kind: "square" | "triangle"
return s.x;
}
} Параметры типов
Как и большинство языков, унаследованных от C, TypeScript требует объявления параметров типов:
function liftArray<T>(t: T): Array<T> {
return [t];
} Требования к регистру нет, но параметры типов традиционно обозначаются одной заглавной буквой. Параметры типов также могут быть ограничены типом, что немного напоминает ограничения типа класса:
function firstish<T extends { length: number }>(t1: T, t2: T): T {
return t1.length > t2.length ? t1 : t2;
} TypeScript обычно может вывести аргументы типов из вызова, основываясь на типе аргументов, поэтому обычно аргументы типов не нужны.
Поскольку TypeScript структурный, ему не так нужны параметры типов, как в номинальных системах. В частности, они не нужны для создания полиморфной функции. Параметры типов следует использовать только для распространения информации о типах, например, для ограничения параметров до одного и того же типа:
function length<T extends ArrayLike<unknown>>(t: T): number {}
function length(t: ArrayLike<unknown>): number {} В первом length, T не требуется; обратите внимание, что он упоминается только один раз, поэтому он не используется для ограничения типа возвращаемого значения или других параметров.
Типы высшего порядка
TypeScript не имеет типов высшего порядка, поэтому следующее не является допустимым:
function length<T extends ArrayLike<unknown>, U>(m: T<U>) {} Программирование без точек
Программирование без точек — интенсивное использование каррирования и композиции функций — возможно в JavaScript, но может быть громоздким. В TypeScript вывод типов часто терпит неудачу для программ без точек, поэтому вам придётся указывать параметры типов вместо параметров значений. Результат настолько громоздкий, что обычно лучше избегать программирования без точек.
Система модулей
Современный синтаксис модулей JavaScript немного похож на синтаксис модулей Haskell, за исключением того, что любой файл с import или export неявно является модулем:
import { value, Type } from "npm-package";
import { other, Types } from "./local-package";
import * as prefix from "../lib/third-package"; Вы также можете импортировать модули commonjs — модули, написанные с использованием системы модулей node.js:
import f = require("single-function-package"); Вы можете экспортировать с помощью списка экспорта:
export { f };
function f() {
return g();
}
function g() {} // g is not exported Или, помечая каждый экспорт индивидуально:
export function f() { return g() }
function g() { } Второй стиль более распространён, но оба допустимы, даже в одном и том же файле.
readonly и const
В JavaScript изменчивость является значением по умолчанию, хотя он допускает объявления переменных с const для объявления неизменяемости ссылки. Ссылаемое значение всё ещё изменяемо:
const a = [1, 2, 3]; a.push(102); // ): a[0] = 101; // D:
TypeScript дополнительно имеет модификатор readonly для свойств.
interface Rx {
readonly x: number;
}
let rx: Rx = { x: 1 };
rx.x = 12; // error Он также поставляется с картой типов Readonly<T> , которая делает все свойства readonly:
interface X {
x: number;
}
let rx: Readonly<X> = { x: 1 };
rx.x = 12; // error И он имеет специальный тип ReadonlyArray<T> , который удаляет методы, вызывающие побочные эффекты, и предотвращает запись в индексы массива, а также специальный синтаксис для этого типа:
let a: ReadonlyArray<number> = [1, 2, 3]; let b: readonly number[] = [1, 2, 3]; a.push(102); // error b[0] = 101; // error
Вы также можете использовать утверждение const, которое работает с массивами и объектами-литералами:
let a = [1, 2, 3] as const; a.push(102); // error a[0] = 101; // error
Однако ни один из этих вариантов не является значением по умолчанию, поэтому они не используются последовательно в коде TypeScript.
Дальнейшие шаги
Этот документ представляет собой краткий обзор синтаксиса и типов, которые вы будете использовать в повседневном коде. Отсюда вы должны:
- Прочитать полный справочник от начала до конца
- Изучить примеры в Плейграунде
© 2012-2023 Microsoft
Licensed under the Apache License, Version 2.0.
https://www.typescriptlang.org/docs/handbook/typescript-in-5-minutes-func.html