Categories
Flow JavaScript

JavaScript Type Checking with Flow — Generics

Spread the love

ow is a type checker made by Facebook for checking JavaScript data types. It has many built-in data types we can use to annotate the types of variables and function parameters.

In this article, we’ll look at how to use generic types to make data types abstract and allows reuse of the code.

Defining Generic Types

To make the types abstract in different entities, we can use generic types to abstract away the types from entities.

For example, we can write a function with generic types as follows:

function foo<T>(obj: T): T {  
  return obj;  
}

In the code above, we have <T> to indicate that foo is a generic function. T is the generic type marker whenever it’s referenced.

To make use of generic types, we have to annotate it. Otherwise, Flow wouldn’t know that it can take a generic type.

We have to annotate the type in type aliases when we want to generic type in type aliases.

For example, we can write:

type Foo = {  
  func<T>(T): T  
}

Then it’ll fail if we write:

type Foo = {  
  func<T>(T): T  
}function foo(value) {  
  return value;  
}const f: Foo = { func: foo };

Since we didn’t add generic type markers to our foo function.

Once we annotate our foo function with generic types, it should work:

type Foo = {  
  func<T>(T): T  
}function foo<T>(value: T): T {  
  return value;  
}const f: Foo = { func: foo };

Syntax

Generic Functions

We can define a generic function as follows:

function foo<T>(param: T): T {  
  
}  
  
function<T>(param: T): T {  
  
}

Also, we can define function types with generics as follows:

<T>(param: T) => T

We can use generic function types with variables and parameters like:

let foo: <T>(param: T) => T = function(param: T): T {}function bar(callback: <T>(param: T) => T) {  
    
}

Generic Classes

To create generic classes, we can insert type placeholders into fields, method parameters, and the return type of methods.

For instance, we can define a generic class as follows:

class Foo<T> {  
  prop: T; constructor(param: T) {  
    this.prop = param;  
  } bar(): T {  
    return this.prop;  
  }  
}

Type Aliases

Generic types can also be added to type aliases. For example, we can write:

type Foo<T> = {  
  a: T,  
  v: T,  
};

Interfaces

Likewise, we can define interfaces with generics as follows:

interface Foo<T> {  
  a: T,  
  b: T,  
}

Passing in Type Arguments

For functions, we can pass in type arguments as follows:

function foo<T>(param: T): T {    
  return param;  
}foo<number>(1);

We can pass in type arguments to classes as well. When we instantiate a class, we can pass in a type argument as follows:

class Foo<T> {}  
const c = new Foo<number>();

One convenient feature of Flow generics is that we don’t have to add all the types ourselves. We can put an _ in place of a type to let Flow infer the type for us.

For instance, we can write the following:

class Foo<T, U, V>{}  
const c = new Foo<_, number, _>()

To let Flow infer the type of T and V.

Behavior

Generic are variables for types. We can use them in place of any data type annotations in our code.

Also, we can name them anything we like, so we can write something like:

function foo<Type1, Type2, Type3>(one: Type1, two: Type2, three: Type3) {  
    
}

Flow tracks the values of variables and parameters annotated with generic types so we can assign something unexpected to it.

For example, we’ll get an error if we write something like:

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

Since we don’t know that if T is a string, we can always return a string when foo is called.

Also, Flow tracks the type of value we pass through a generic so we can use it later:

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

let one: 1 = foo(1);

Notice that we didn’t pass in any generic type argument for it to identify the type as 1. Changing 1 to number also works:

let one: number = foo(1);

If we omit the generic type argument in a generic function, Flow will let us pass in anything:

function logBar<T>(obj: T): T {  
  if (obj && obj.bar) {  
    console.log(obj.bar);  
  }  
  return obj;  
}

logBar({ foo: 'foo', bar: 'bar' });    
logBar({ bar: 'bar' });

We can restrict what we can pass in by specifying restrictions for the type parameter:

function logBar<T: { bar: string }>(obj: T): T {  
  if (obj && obj.bar) {  
    console.log(obj.bar);  
  }  
  return obj;  
}

logBar({ foo: 'foo', bar: 'bar' });    
logBar({ bar: 'bar' });

After adding { bar: string } , then we know that anything passed in must have the stringbar property.

We can do the same for primitive values:

function foo<T: number>(obj: T): T {  
  return obj;  
}

foo(1);

If we try to pass in data of any other type, it’ll fail, so

foo('2');

will get us an error.

Generics lets us return a more specific type than we specify in the type parameter. For example, if we have a string function that returns the value that’s passed in:

function foo<T: string>(val: T): T {  
  return val;  
}

let f: 'foo' = foo('foo');

Instead of assigning something to a string variable, we can assign something that has the returned value of the function as the type.

Parameterized Generics

We can pass in types to generics like we pass arguments to a function. We can do this with type alias, functions, interfaces, and classes. This is called a parameterized generic.

For example, we can make a parameterized generic type alias as follows:

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

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

Likewise, we can do the same for classes:

class Foo<T> {  
  value: T;  
  constructor(value: T) {  
    this.value = value;  
  }  
}

let foo: Foo<string> = new Foo('foo');

For interfaces, we can write:

interface Foo<T> {  
  prop: T,  
}

class Item {  
  prop: string;  
}(Item.prototype: Foo<string>);

Default Type for Parameterized Generics

We can add default values to parameterized generics. For example, we can write:

type Item<T: string = 'foo'> = {  
  prop: T,  
};

let foo: Item<> = { prop: 'foo' };

If the type isn’t specified, then prop is assumed to have the 'foo' type.

That means that any other value for prop won’t work if we leave the type argument blank. So something like:

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

won’t be accepted.

Variance Sigils

We can use the + sign to allow for broader types than the assigned value’s type when the casting types of generics. For example, we can write:

type Foo<+T> = T;let x: Foo<string> = 'foo';  
(x: Foo<number| string>);

As we can see, we can convert x from type Foo<string> to Foo<number| string> with the + sign on the generic Foo type.

With generics, we can abstract the type out of our code by replacing them with generic type markers. We can use them anywhere that has type annotations. Also, it works with any Flow constructs like interfaces, type alias, classes, variables, and parameters.

By John Au-Yeung

Web developer specializing in React, Vue, and front end development.

Leave a Reply

Your email address will not be published. Required fields are marked *