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; }
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
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 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 Box. U, 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)
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:
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)
Post a Comment