Generic Types

Generic Types

Generics (sometimes referred to as polymorphic types) are a way of abstracting a type away.

Imagine writing the following identity function which returns whatever value was passed.

function identity(value) {
  return value;
}

We would have a lot of trouble trying to write specific types for this function since it could be anything.

function identity(value: string): string {
  return value;
}

Instead we can create a generic (or polymorphic type) in our function and use it in place of other types.

function identity<T>(value: T): T {
  return value;
}

Generics can be used within functions, function types, classes, type aliases, and interfaces.

Syntax of generics

There are a number of different places where generic types appear in syntax.

Functions with generics

Functions can create generics by adding the type parameter list <T> before the function parameter list.

You can use generics in the same places you’d add any other type in a function (parameter or return types).

function method<T>(param: T): T {
  // ...
}

function<T>(param: T): T {
  // ...
}
Function types with generics

Function types can create generics in the same way as normal functions, by adding the type parameter list <T> before the function type parameter list.

You can use generics in the same places you’d add any other type in a function type (parameter or return types).

<T>(param: T) => T

Which then gets used as its own type.

function method(func: <T>(param: T) => T) {
  // ...
}
Classes with generics

Classes can create generics by placing the type parameter list before the body of the class.

class Item<T> {
  // ...
}

You can use generics in the same places you’d add any other type in a class (property types and method parameter/return types).

class Item<T> {
  prop: T;

  constructor(param: T) {
    this.prop = param;
  }

  method(): T {
    return this.prop;
  }
}
Type aliases with generics
type Item<T> = {
  foo: T,
  bar: T,
};
Interfaces with generics
interface Item<T> {
  foo: T,
  bar: T,
}

Behavior of generics

Generics act like variables

Generic types work a lot like variables or function parameters except that they are used for types. You can use them whenever they are in scope.

function constant<T>(value: T) {
  return function(): T {
    return value;
  };
}

Create as many generics as you need

You can have as many of these generics as you need in the type parameter list, naming them whatever you want:

function identity<One, Two, Three>(one: One, two: Two, three: Three) {
  // ...
}

Generics track values around

When using a generic type for a value, Flow will track the value and make sure that you aren’t replacing it with something else.

// @flow
function identity<T>(value: T): T {
  // $ExpectError
  return "foo"; // Error!
}

function identity<T>(value: T): T {
  // $ExpectError
  value = "foo"; // Error!
  // $ExpectError
  return value;  // Error!
}

Flow tracks the specific type of the value you pass through a generic, letting you use it later.

// @flow
function identity<T>(value: T): T {
  return value;
}

let one: 1 = identity(1);
let two: 2 = identity(2);
// $ExpectError
let three: 3 = identity(42);

Adding types to generics

Similar to mixed, generics have an “unknown” type. You’re not allowed to use a generic as if it were a specific type.

// @flow
function logFoo<T>(obj: T): T {
  // $ExpectError
  console.log(obj.foo); // Error!
  return obj;
}

You could refine the type, but the generic will still allow any type to be passed in.

// @flow
function logFoo<T>(obj: T): T {
  if (obj && obj.foo) {
    console.log(obj.foo); // Works.
  }
  return obj;
}

logFoo({ foo: 'foo', bar: 'bar' });  // Works.
logFoo({ bar: 'bar' }); // Works. :(

Instead, you could add a type to your generic like you would with a function parameter.

// @flow
function logFoo<T: { foo: string }>(obj: T): T {
  console.log(obj.foo); // Works!
  return obj;
}

logFoo({ foo: 'foo', bar: 'bar' });  // Works!
// $ExpectError
logFoo({ bar: 'bar' }); // Error!

This way you can keep the behavior of generics while only allowing certain types to be used.

// @flow
function identity<T: number>(value: T): T {
  return value;
}

let one: 1 = identity(1);
let two: 2 = identity(2);
// $ExpectError
let three: "three" = identity("three");

Generic types act as bounds

// @flow
function identity<T>(val: T): T {
  return val;
}

let foo: 'foo' = 'foo';           // Works!
let bar: 'bar' = identity('bar'); // Works!

In Flow, most of the time when you pass one type into another you lose the original type. So that when you pass a specific type into a less specific one Flow “forgets” it was once something more specific.

// @flow
function identity(val: string): string {
  return val;
}

let foo: 'foo' = 'foo';           // Works!
// $ExpectError
let bar: 'bar' = identity('bar'); // Error!

Generics allow you to hold onto the more specific type while adding a constraint. In this way types on generics act as “bounds”.

// @flow
function identity<T: string>(val: T): T {
  return val;
}

let foo: 'foo' = 'foo';           // Works!
let bar: 'bar' = identity('bar'); // Works!

Note that when you have a value with a bound generic type, you can’t use it as if it were a more specific type.

// @flow
function identity<T: string>(val: T): T {
  let str: string = val; // Works!
  // $ExpectError
  let bar: 'bar'  = val; // Error!
  return val;
}

identity('bar');

Parameterized generics

Generics sometimes allow you to pass types in like arguments to a function. These are known as parameterized generics (or parametric polymorphism).

For example, a type alias with a generic is parameterized. When you go to use it you will have to provide a type argument.

type Item<T> = {
  prop: T,
}

let item: Item<string> = {
  prop: "value"
};

You can think of this like passing arguments to a function, only the return value is a type that you can use.

Classes (when being used as a type), type aliases, and interfaces all require that you pass type arguments. Functions and function types do not have parameterized generics.

Classes

// @flow
class Item<T> {
  prop: T;
  constructor(param: T) {
    this.prop = param;
  }
}

let item: Item<number> = new Item(42); // Works!
// $ExpectError
let item: Item = new Item(42); // Error!

Type Aliases

// @flow
type Item<T> = {
  prop: T,
};

let item: Item<number> = { prop: 42 }; // Works!
// $ExpectError
let item: Item = { prop: 42 }; // Error!

Interfaces

// @flow
interface HasProp<T> {
  prop: T,
}

class Item {
  prop: string;
}

(Item.prototype: HasProp<string>); // Works!
// $ExpectError
(Item.prototype: HasProp); // Error!
Adding defaults to parameterized generics

You can also provide defaults for parameterized generics just like parameters of a function.

type Item<T: number = 1> = {
  prop: T,
};

let foo: Item<> = { prop:1 };
let bar: Item<2> = { prop: 2 };

You must always include the brackets <> when using the type (just like parentheses for a function call).

© 2013–present Facebook Inc.
Licensed under the BSD License.
https://flow.org/en/docs/types/generics

在线笔记
App下载
App下载

扫描二维码

下载编程狮App

公众号
微信公众号

编程狮公众号

意见反馈
返回顶部