I. Existence Significance
Consider such a scenario, identity function accepts a parameter, and returns it as is:
function identity(arg) {
return arg;
}
From type perspective, no matter what type the parameter is, the return value type is consistent with the parameter, with help of [overload mechanism](/articles/函数-typescript 笔记 5/#articleHeader9), can describe like this:
function identity(arg: number): number;
function identity(arg: string): string;
function identity(arg: boolean): boolean;
// ...etc countless a => a type descriptions
Overload seems cannot satisfy this scenario, because we have no way to enumerate all possible types of arg. Since parameter is any type, might as well try with any:
function identity(arg: any): any;
Covers all types, but lost the type correspondence relationship between parameter and return value (above is equivalent to A => B type mapping, but what we want to describe is A => A)
Generics and any
So, how should we express the correspondence relationship between two anys?
Use generics. Describe like this:
function identity<T>(arg: T): T {
return arg;
}
Type variable T is similar to any, equivalent to named any, this way can clearly express T => T (i.e. A => A) type mapping
II. Type Variables
Type variable, a special kind of variable that works on types rather than values.
Ordinary variables represent a value, while type variables represent a type
From function perspective, variables can transport values, while type variables transport type information:
This allows us to traffic that type information in one side of the function and out the other.
III. Generic Functions
Type variables are also called type parameters, similar to function parameters, difference is function parameters accept a concrete value, while type parameters accept a concrete type, for example:
function identity<T>(arg: T): T {
return arg;
}
// Pass argument to type parameter
// identity<number>
// Pass argument to function parameter (automatically infer type parameter)
identity(1);
// Pass argument to function parameter (explicitly pass type parameter)
identity<number>(1);
Functions with type parameters are called generic functions, where type parameters represent any type (any and all types), so only features common to all types can be accessed:
function loggingIdentity<T>(arg: T): T {
// Error Property 'length' does not exist on type 'T'.
console.log(arg.length);
return arg;
}
Actually, because there's void this empty set, so there don't exist properties or methods common to all types. Also cannot make any assumptions about type variables (such as assuming it has length property), because it represents an arbitrary type, without any constraints
Besides this, type variable T is just like a concrete type, can be used anywhere concrete types appear:
function loggingIdentity<T>(arg: T[]): T[] {
console.log(arg.length); // Array has a .length, so no more error
return arg;
}
// Or
function loggingIdentity<T>(arg: Array<T>): Array<T> {
console.log(arg.length); // Array has a .length, so no more error
return arg;
}
Type Descriptions
Generic function type descriptions are similar to ordinary functions:
// Ordinary function
let myIdentity: (arg: string) => string =
function(arg: string): string {
return arg;
};
// Generic function
let myIdentity: <T>(arg: T) => T =
function<T>(arg: T): T {
return arg;
};
Still arrow function syntax, just added <type parameter list> before (parameter list). Similarly, type parameter names in type descriptions can also be inconsistent with actual ones:
let myIdentity: <U>(arg: U) => U =
function<T>(arg: T): T {
return arg;
};
P.S. Specially, function type descriptions can also be written in object literal form:
// Generic function
let myIdentity: { <T>(arg: T): T };
// Ordinary function
let myIdentity: { (arg: string): string };
Like a degraded version of interface form type descriptions, no reuse advantage, also not as concise as arrow functions, therefore, not common
IV. Generic Interfaces
Interfaces with type parameters are called generic interfaces, for example can use interface to describe a generic function:
interface GenericIdentityFn {
<T>(arg: T): T;
}
There's also a very similar form:
interface GenericIdentityFn<T> {
(arg: T): T;
}
Both are called generic interfaces, difference is latter's type parameter T acts on entire interface, for example:
interface GenericIdentity<T> {
id(arg: T): T;
idArray(...args: T[]): T[];
}
let id: GenericIdentity<string> = {
id: (s: string) => s,
// Error Types of parameters 's' and 'args' are incompatible.
idArray: (...s: number[]) => s,
};
Interface-level type parameters have this constraint effect, member-level ones don't (only act on that generic member)
V. Generic Classes
Similarly, classes with type parameters are called generic classes, for example:
class GenericNumber<T> {
zeroValue: T;
add: (x: T, y: T) => T;
}
Like interfaces, generic classes can constrain that all members of the class focus on consistent target types:
Putting the type parameter on the class itself lets us make sure all of the properties of the class are working with the same type.
Note, type parameters only apply to instance members in classes, static members cannot use type parameters, for example:
class GenericNumber<T> {
// Error Static members cannot reference class type parameters.
static zeroValue: T;
}
Because static members are shared among class instances, cannot uniquely determine concrete type of type parameters:
let n1: GenericNumber<string>;
// Expect n1.constructor.zeroValue to be string
let n2: GenericNumber<number>;
// Expect n1.constructor.zeroValue to be number, appears contradiction
P.S. This point is consistent with Java, specifically see Static method in a generic class?
VI. Generic Constraints
Type parameters are too "generic" (any and all), in some scenarios, may want to add constraints, for example:
interface Lengthwise {
length: number;
}
function loggingIdentity<T extends Lengthwise>(arg: T): T {
console.log(arg.length); // Now we know it has a .length property, so no more error
return arg;
}
Use interfaces to describe constraints on type parameters (T extends constraintInterface), such as above requires type parameter T must have a length property of number type
Another typical scenario is factory methods, for example:
// Require constructor c must return instances of same class (or subclass)
function create<T>(c: {new(): T; }): T {
return new c();
}
Additionally, can also use type parameters in generic constraints, for example:
function getProperty<T, K extends keyof T>(obj: T, key: K) {
return obj[key];
}
let x = { a: 1, b: 2, c: 3, d: 4 };
getProperty(x, "a"); // okay
getProperty(x, "m"); // error: Argument of type 'm' isn't assignable to 'a' | 'b' | 'c' | 'd'.
Can use one type parameter's features to constrain another type parameter, quite powerful
VII. Summary
It's called generics because it can act on a series of types, it's a layer of abstraction above concrete types:
Generics are able to create a component that can work over a variety of types rather than a single one. This allows users to consume these components and use their own types.
Reference Materials
-
[Type Parameters | Types_Haskell Notes 3](/articles/类型-haskell 笔记 3/#articleHeader9)
No comments yet. Be the first to share your thoughts.