Небольшое примечание: этот пост отмечен тегом Flow, но все, кроме последнего набора примеров, переводится непосредственно на TypeScript 2+ (с включенным --strictNullChecks
). Кроме того, этот пост скопирован из моего блога, потому что я не очень-то люблю самообслуживание.
«Параметризованные типы» — это особенность Flow, TypeScript и растущей коллекции других языков. Это способ пробить дыры в определении типа, предоставляя более позднему пользователю возможность заполнить пробелы конкретными типами. Это инструмент, позволяющий вам создавать общие и повторно используемые определения типов, а также инструмент, с помощью которого мы можем работать над функциями, исключая детали, которые нам не нужны.
Это немного сложно объяснить в одном-двух предложениях, так что давайте сразу к делу. Вот несколько объявлений типов:
// @flow
type JustAString = string; type StringOrNumber = string | number; type Optional<X> = X | null;
У нас есть:
- Тип
JustAString
, представляющий значение, которое может быть любой строкой, но не более того. - Тип
StringOrNumber
, представляющий значение, которое может быть любым числом или любой строкой, но не более того. - Более общий тип,
Optional<X>
. Оно обозначает либо что-то (X
, мы пока не уточняем, что), либоnull
, но больше ничего.
Мы называем это X
«параметром типа». X
не является особым именем: это может быть T
, или Item
, или Thing
, кому какое дело. Мы углубимся в то, что на самом деле означает «параметр типа», чуть позже, но сначала давайте переключим передачу и на мгновение подумаем о функциях.
Скажем, у нас есть это:
function doubleIt(x) {
return x + x;
}
Это функция doubleIt
, которая имеет «параметр» x
. Мы вызываем функцию, предоставляя значения для ее параметров функции:
const result = doubleIt(5);
// ... is like taking this: function doubleIt(x) { return x + x; }
// ... is like subbing in the variables like this: function doubleIt(5) { return 5 + 5; }
// ... to get a value: 10;
Точно так же мы можем специализировать тип, предоставляя типы для его параметров типа:
type OptionalNumber = Optional<number>;
// ... is like taking this: type Optional<X> = X | null;
// ... and subbing in the types like this: type Optional<number> = number | null;
// ... to get a type representing "a number, or null". const someNumber: Optional<number> = 10; const isJustNull: Optional<number> = null;
// Kaboom; doesn't pass Flow's type-check. const gonnaBreak: Optional<number> = "💥";
Как и выше, X
не является специальным именем: это просто то, что мы назвали параметром типа, точно так же, как x
— это то, что мы назвали параметром функции.
Как и параметры функции, X
(например) относится к одному типу. Тип type Point<T> = { x: T, y: T }
относится к объекту с двумя полями, x
и y
, каждое из которых имеет значение одного и того же типа (T
, независимо от того, для чего он впоследствии будет специализирован).
Также поддерживаются несколько параметров, например. type Post<ID,Title> = { id: ID, title: Title }
. Они могут быть одного типа (например, строка) или совершенно разных (например, число и строка), но мы не знаем, пока они не будут специализированы для каждого использования, поэтому мы не можем предположить, что они перекрываются.
Идем дальше. Эти параметры типа работают с множеством вещей, например. функции:
// A function that takes a T, and returns a value of that same type. function justReturn<T>(x: T): T { return x; }
const hello = justReturn("Hello");
// ... is like subbing in the type like this: function justReturn<string>(x: string): string { return x; }
Or:
function first<X>(items: Array<X>): Optional<X> { if (0 in items) { return items[0]; } else { return null; } }
const nine = first([9, 8, 7]);
// ... specialising to a type like: function first<number>(items: Array<number>): Optional<number> { // ... }
// Which in turn expands to: function first<number>(items: Array<number>): (number | null) { // ... }
Заметили кое-что о first
? Мы никогда не смотрели на содержимое элементов, только на сам массив. Функция, как написано, не знает, какого типа элементы; только при использовании мы можем специализировать items
как массив чисел, строк, объектов и т. д.
Возьмем последний пример:
function first<X>(items: Array<X>): Optional<X> { if (0 in items) { // Add 100 to each array item. But what is the type of // item? We don't know, and if we don't know, we // can't tell if this is correct, so... Kaboom. 💥 return items[0] + 100; } else { return null; } }
// ... This fails to type-check with: // // 15: function first<T>(items: Array<T>): Optional<T> { // ^ T. This type cannot be added to // 17: return items[0] + 100; // Add 100 to each item // ^ number
Наша функция first
не может знать, что это за элементы [см. примечание 1 ниже]. Мы уже используем Flow, чтобы ограничить данные такими типами, как числа и строки. Не позволяя first
возиться с самими элементами, мы ограничили функцию, и тип функции (записанный в терминах параметра типа T
) отражает это ограничение.
Как и в случае с большей частью того, что мы делаем с Flow, чем более ограниченными мы можем сделать типы наших данных и функций, тем меньше возможностей нам нужно учитывать при их изменении, что позволяет нам сосредоточиться на том, что мы сейчас делаем. с меньшим количеством ошибок. Параметризованные типы позволяют нам делать это, заставляя нас беспокоиться только о том, что нас волнует (наш ввод — это массив), а не о вещах, которые нам не нужны (это массив чисел).
Я оставлю вам несколько примеров параметризованных типов, которые я считаю полезными. Мне интересно услышать, какие еще комбинации вы придумали. 😊
// This is Flow's ? symbol, eg. Q<string> is ?string. type Q<X> = X | null | undefined;
// This is a function that returns a Promise; we've used a type // parameter to specify that the returned value is going to // be a string. function fetchExampleDotCom(): Promise<string> { return fetch('http://example.com') .then(response => response.text()) }
// Inspired by Rust; this is either a failure or a success. // Coming up with a generic success/failure type like this means // you can write helper functions for them, and reuse them for // many cases of success or failure. type Result<Error,Value> = { error: Error } | { value: Value }
// An AJAX request state, inspired by @krisjenkin's post here: // http://blog.jenkster.com/2016/06/how-elm-slays-a-ui-antipattern.html type RequestData<Error,Value> = { type: "notRequested" } | { type: "requesting" } | { type: "failure", error: Error } | { type: "success", value: Value }
// eg. // type RemoteTodoItems = RequestData<string, Array<TodoItem>>;
// Something from the next post. :) // This lets us create a named wrapper around string (for User // IDs, in this case) that prevents us from mixing up with // other strings. class UserID extends TypeWrapper<string> {}
PS: Последнее замечание по терминологии. На случай, если вы столкнетесь с этим в будущем, концепция наличия этих параметризованных типов иногда называется «Generics» (см. Java, C#, дебаты Go «Generics» и т. д.) под названием «Параметричность» [см. примечание 2].
1. В Flow есть (неудачный? Специально разработанный?) бэкдор, который позволяет вам выяснить, что это за элементы; Я расскажу об этом в следующем посте.
2. И Flow, и TypeScript пропускают код, нарушающий параметричность, поэтому я не хочу использовать этот термин; на тему this Gist есть кое-что взад и вперед.