Wednesday, January 13, 2021

Generic Constraints, Generic Methods, and Generic Factories in TypeScript

In Introduction to Generics in TypeScript the idea of Generics was introduced. It was shown how Generics provide a mechanism for having a balance between writing code that is flexible enough to apply to a broad range of types but not too flexible that type safety is lost. 

This post will be a short one that builds on that and shows a couple of extra things that can be achieved when using Generics in TypeScript. Three things will be shown in this post: Generic Constraints, Generic Methods, and Generic Factories

Generic Constraints


In Introduction to Generics in TypeScript we saw an example of how Generics can be used to implement an identity function. By using Generics we were able to come up with an implementation that works with values of all types.

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

Sometimes this is not what we want. Sometimes we want to limit the types of values we accept for an implementation. For example, we might want to have an implementation of the identity function that only works on values with a name attribute. For such situations, we use generic constraints.

Generic constraints allow us to place a constraint on the type of values that are allowed within our generic implementation.

An implementation of the identity function that constrains the values it accepts to types with name attribute can be implemented as follows:

type Nameable = {
  name: string
}

function identity<T extends Nameable>(input: T) {
  return input;
}

identity({name:"John Doe", age: 23})

The main syntax is T extends Nameable. The general idea is, instead of having an unconstrained type variable T, the type variable is constrained by specifying whatever T is, it should be a type that extends Nameable

Hence when the identity function is attempted to be called with a number it does not compile.

// Argument of type 'number' is not assignable 
// to parameter of type '{ name: string; }'//identity(3)


Generic methods


In Introduction to Generics in TypeScript, we have seen that we can use Generics when defining a class and a function. It is also possible to have generic methods. That is using Generics when defining the functions on a class. 

The thing to note when using such a generic method is that the type specified via the generic variable on the class is distinct and different from the ones specified via the generic on the method. 

To illustrate this, we define a generic data structure, Box that can contain anything. This data structure will then have a generic method that can be used to transform the content of the Box into another generic. type.

class Box <T> {
  private item: T

  constructor(thing: T) {
    this.item = thing;
  }

  get():T{
    return this.item
  }

  transform<U>(f: (item:T) => U) {
    return f(this.item)
  }
}

The generic variable specified via the class applies to the instance of the object. This means on the creation of the instant of the class above, the generic value is set. 

The type to transform it into is not set. This is only set at the point when the instance method is called. 

For example in the code snippet below, the generic T is set to number on creation, because 2, a number is passed to the constructor of BoxU, on the other hand is not set until the transform method is called which then set U to string.

let abox = new Box(2)
let res = abox
            .transform<string>((input) => { 
                return `Transformed to ${input}`; 
             })

// prints Transformed to 2
console.log(res)

While in this other code snippet, when transform is called, the generic U is set to number

let bbox = new Box("1")
let res = bbox.transform<number>((input) => { 
  return parseInt(input) + 100; 
})

// prints 101
console.log(res)


Generic Factories


A factory is a function that is used to create values of a particular type. In some cases, this is preferred to using constructors for various reasons.

A factory will at some point call the constructor of the value it needs to create, given this fact, the question is: is it possible to have a generic factory in Typescript? 

Yes. This is because TypeScript provides a way to specify types for a constructor. This looks like this:

type Constructor<T> = { new (): T }

Which is the call signature syntax with the new keyword inserted. If you are unfamiliar with the phrase, call signature, then check out the post: How to apply type annotations to functions in TypeScript

What about in the cases where the constructor takes an argument?

Well this syntax can also handle such situations, for example:
 
type Constructor<T> = {new (arg1: number, arg2: string): T }

Being able to specify type signature of constructor provides an ability to have generic factories. An example that demonstrates how to do this and what can be achieved using a generic factory is shown below:

class Person {
  public age: number = NaN;
  public name: string = "";
}

class Circle {
  public radius: number = NaN;
}

type Constructor<T> = {new ():T}

function createAndInit<T, D>(target:Constructor<T>, decoration: D): T {
  return Object.assign(new target, decoration)
}

let person = createAndInit(Person, { age: 18, name: "John Doe" })

// prints
// Person: {
//  "age": 18,
//  "name": "John Doe"
// } 
console.log(person)

let circle = createAndInit(Circle, { radius: 20 })

// prints
// Circle: {
//  "radius": 20
// } 
console.log(circle)

In the code above the createAndInit function makes use of a generic factory as a constructor to create an instance of Person and Circle objects.


This post is part of a series. See Introduction to Advance Types in TypeScript for more posts in the series.


I am writing a book: TypeScript Beyond The Basics. Sign up here to be notified when it is ready.

1 comment:

Ździchu said...

Im begginer in Typescript and I have an error in the return Object.assign(new target(), decoration) line code

Type '{} & D' is not assignable to type 'T'.
'T' could be instantiated with an arbitrary type which could be unrelated to '{} & D'.ts(2322)