function identify (arg: number): number {
return arg;
}
Of course, in TS, we can specify any so that the functioncan be used for any data type:
function identify (arg: any): any {
return arg;
}
We can say that this function is a generalization, since it can work with any type of data. But in this case, we have absolutely no information about the type of the returned value.
When using generalizations, we can explicitly specify the type of the argument and thus, within the framework of our example, determine the type of the return value.
function identify<T>(arg: T): T {
return arg;
}
let numberOutput = identify<number>(5);
let wrongOutput = identify<number>("5"); // Error
let stringOutput = identify<string>("5"); // OK
When using generalizations, the compiler assumes that the generalized parameters passed to the function are used correctly. In fact, the compiler treats them as “any data type” or any.
You can describe it as if we are creating an array:
function loggingIdentity<T> (arg: T[]): T[] {
console.log(arg.length); // Array has a .length, so no error
return arg;
}
This allows us to use the generalized type variable Tas part of the type we are working with, rather than just as an entire type, which gives us more flexibility.
function loggingIdentity<T>(arg: Array<T>): Array<T> {
console.log(arg.length); // Array has a .length, so no error
return arg;
}
Defining a generic function type is very similar to defining a type for a regular function. The only difference is that you first need to specify the type of parameters to be passed and the return value, just like when creating a generalized function:
function identity<T>(arg: T): T {
return arg;
}
let myIdentify: <T>(arg: T) => T = identity;
A different name could be used for the standard parameter, but it is only important that the number of standard parameters and how they are used are consistent.
function identify<T>(arg: T): T {
return arg;
}
let myIdentify: <U>(arg: U) => U = identify;
You can also write a generic type as a call signature on the type of an object literal:
function identity<T>(arg: T): T {
return arg;
}
let myIdentify: {<T>(arg: T): T} = identity;
To the description of the first generalized interface.
interface GenericIdentityFn {
<T>(arg: T): T;
}
function identity<T>(arg: T): T {
return arg;
}
let myIdentity: GenericIdentityFn = identity;
Moreover, we can specify a generic type for the entire interface, which will make this parameter available to all its methods.
interface GenericIdentityFn<T> {
(arg: T): T;
}
function identity<T>(arg: T): T {
return arg;
}
let myIdentity: GenericIdentityFn<number> = identity;
In addition to generic interfaces, you can also create generic classes.
Note that you cannot create generic enumerations andnamespaces.
Generalized classes have the same form as generalized interfaces. They have a list of typical parameters in angle brackets (<>
) after the class name.
class GenericNumber<T> {
zeroValue: T;
add: (x: T, y: T) => T;
}
let myGenericNumber = new GenericNumber<number>();
myGenericNumber.zeroValue = 0;
myGenericNumber.add = function(x, y) { return x + y; };
This is a fairly literal use of the GenericNumber type (lit.a generalized number), but you can see that nothing prevents you from using other types with it, except number.
let stringNumeric = new GenericNumber<string>();
stringNumeric.zeroValue = "";
stringNumeric.add = function(x, y) { return x + y; };
console.log(stringNumeric.add(stringNumeric.zeroValue, "test")); // test
A class has two types: the static part type and the instance type. Generic types are such only in relation to the instance type, but not to the static part type. Therefore, static class members cannot use typical class parameters.
function loggingIdentity<T>(arg: T): T {
console.log(arg.length); // Error! T doesn't have .length property
return arg;
}
Instead of working with any possible type, we would like to create a constraint so that the function works with all types that have a property .length
.
interface Lengthwise {
length: number;
}
function loggingIdentity<T extends Lengthwise>(arg: T): T {
console.log(arg.length); // Now we know it has a .length property
return arg;
}
You can define a type parameter that will be limited to the type of another type parameter.
function getProperty<T, K extends keyof T>(obj: T, key: K) {
return obj[key];
}
let x = { a: 1, b: 2, c: 3, d: 4 };
console.log(getProperty(x, "a")); // okay
console.log(getProperty(x, "m")); // error: Argument of type 'm' isn't assignable to 'a' | 'b' | 'c' | 'd'.
When creating object factories in TS using generalizations, you must refer to class types in constructor functions.
function create <T>(c: {new(): T; }): T {
return new c();
}
class SomeClass { }
var obj = create(SomeClass);
console.log(obj.constructor.name); // "SomeClass"
The following example shows how to impose restrictions on the types of classes created using the class factory:
class BeeKeeper {
hasMask: boolean = false;
}
class ZooKeeper {
nametag: string = "tag";
}
class Animal {
numLegs: number = 0;
}
class Bee extends Animal {
keeper: BeeKeeper = new BeeKeeper();
}
class Lion extends Animal {
keeper: ZooKeeper = new ZooKeeper();
}
function createInstance<TAnimal extends Animal>(c: new () => TAnimal): TAnimal {
return new c();
}
console.log(createInstance(Lion).keeper.nametag); // tag
console.log(createInstance(Bee).keeper.hasMask); // false
To create a new object in the generalization code, we need to specify that the generalized type T
has a constructor. This means that instead of the type: T
parameter, we need to specify type: {new(): T;}
.
function UserFactory <T>(): T {
return new T(); // Error: 'T' only refers to a type, but is being used as a value here.
}
To make the interface work, use the word new
:
function UserFactory<T> (type: {new(): T;}): T {
return new type();
}