Небольшое примечание: этот пост отмечен тегом 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 есть кое-что взад и вперед.