Generics
Motivation
Several language elements may be declared in a generic form; we’ll start with focusing on classes, generic methods will be discussed after that.
The standard case, of course, is a non-generic class. Take the following class, for example, that bundles a pair of two strings:
export public class PairOfString {
first: string;
second: string;
}
This implementation is fine as long as all we ever want to store are strings. As experience shows, someone is soon to come up with the idea of storing two values of another type, such as… numbers! Let’s add another class:
export public class PairOfNumber {
first: number;
second: number;
}
Following this pattern of adding more classes for new types to be stored obviously has its limitations. We would soon end up with a multitude of classes that are basically serving the same purpose, leading to code duplication, terrible maintainability and many other problems.
One solution could be having a class that stores two values of type any
(in N4JS,
any
is the so-called 'top type', the common supertype of all other types).
export public class PairOfWhatEver {
first: any;
second: any;
}
Now we’re worse off than before. We have lost the certainty that within a single pair, both values will always be of the same type. When reading a value from a pair, we have no clue what its type might be.
Generic Classes (and Interfaces)
The way to solve our previous conundrum using generics is to introduce a type variable for the class We will then call such a class a generic class. A type variable can then be used within the class declaration just as any other ordinary type.
export public class Pair<T> {
first: T;
second: T;
}
The type variable T
, declared after the class name in angle brackets, now represents
the type of the values stored in the Pair
and can be used as the type of the two fields.
Now, whenever we refer to the class Pair
, we will provide a type argument, in other words a
type that will be used wherever the type variable T
is being used inside the class
declaration.
import { Pair } from 'Pair';
let myPair = new Pair<string>();
myPair.first = '1st value';
myPair.second = '2nd value';
By using a type variable, we have not just allowed any given type to be used as value type,
we have also stated that both values, first and second, must always be of the same type. We
have also given the type system a chance to track the types of values stored in a Pair
:
import { Pair } from 'Pair';
let myPair2 = new Pair<string>();
myPair2.first = '1st value';
myPair2.second = 42; // error: 'int is not a subtype of string.'
console.log(myPair2.first.charAt(2));
// type system will know myPair2.first is of type string
The error in line 3 shows that the type checker will make sure we won’t put any value of incorrect
type into the pair. The fact that we can access method charAt()
(available on strings)
in the last line indicates that when we read a value from the pair, the type system knows its type
and we can use it accordingly.
Generic interfaces can be declared in exactly the same way.
Generic Functions (and Methods)
With the above, we can now avoid introducing a multitude of classes that are basically serving the same purpose. It is still not possible, however, to write code that manipulates such pairs regardless of the type of its values may have. For example, a function for swapping the two values of a pair and then return the new first value would look like this:
import { PairOfString } from 'PairOfString';
function swapStrings1(pair: PairOfString): string {
let backup = pair.first; // inferred type of 'backup' will be string
pair.first = pair.second;
pair.second = backup;
return pair.first;
}
The above function would have to be copied for every value type to be supported. Using the generic class
Pair<T>
does not help much:
import { Pair } from 'Pair';
function swapStrings2(pair: Pair<string>): string {
let backup = pair.first; // inferred type of 'backup' will be string
pair.first = pair.second;
pair.second = backup;
return pair.first;
}
The solution is not only to make generic the type being manipulated generic (as we have done with class
Pair<T>
above) but to make the code performing the manipulation generic:
import { Pair } from 'Pair';
function <T> swap(pair: Pair<T>): T {
let backup = pair.first; // inferred type of 'backup' will be T
pair.first = pair.second;
pair.second = backup;
return pair.first;
}
We have introduced a type variable for function swap()
in much the same way as
we have done for class Pair
in the previous section (we then call such a function
a generic function). Similarly, we can use the type variable in this function’s signature
and body.
It is possible to state in the declaration of the function swap()
above that
it will return something of type T
when having obtained a Pair<T>
without
even knowing what type that might be. This allows the type system to track the type of values passed
+between functions and methods or put into and taken out of containers and so on.
Generic methods can be declared just as generic functions. There is one caveat, however: Only if a method introduces its own, new type variables is it called a generic method. If it is merely using the type variables of its containing class or interface, it’s an ordinary method. The following example illustrates the difference:
export public class Pair<T> {
…
public foo(): T { … }
public <S> bar(pair: Pair2<S>): void { … }
}
The first method foo
is a non generic method, while the second one bar
is.
A very interesting application of static methods is when using in combination with function type arguments:
class Pair<T> {
…
<R> merge(merger: {function(T,T): R}): R {
return merger(this.first, this.second);
}
}
var p = new Pair<string>();
…
var i = p.merge( (f,s)=> f.length+s.length )
You will notice that N4JS can infer the correct types for the arguments and the return type of the arrow expression. Also the type for i
will be automatically computed.
Differences to Java
Important differences between generics in Java and N4JS include:
-
Primitive types can be used as type arguments in N4JS.
-
There are no raw types in N4JS. Whenever a generic class or interface is referenced, a type argument has to be provided - possibly in the form of a wildcard. For generic functions and methods, an explicit definition of type arguments is optional if the type system can infer the type arguments from the context.