Sometimes you can have different types for the same logic. The easiest example of this case is identity
function:
function identity(value) {return value;}
If you try to annotate only types with which you currently use this function you will have multiple declaration of the same function:
function identityString(value: string): string {return value;}function identityNumber(value: number): number {return value;}function identityBoolean(value: boolean): boolean {return value;}// Type of num is "number"let num = identityNumber(2);// Type of str is "string"let str = identityString("2");// Type of bool is "boolean"let bool = identityBoolean(false);
As you can see, you need to redefine the identity
function only for type safety. These functions do not add new business value to your code. You can try to solve this problem with Unknown Type, but you will lose the type of the return value.
function identity(value: unknown): unknown {return value;}// Type of num is "unknown"let num = identity(2);// Type of str is "unknown"let str = identity("2");// Type of bool is "unknown"let bool = identity(false);
And then Generic Types appear.
Generic Types are the way to create something like a "type function". You can define "type arguments" which can be applied to this "type function" and a new type will be returned.
// "T" is type variablefunction identity<T>(value: T): T {return value;}// T will be replaced by "number".// Type of num is "number"let num = identity<number>(2);// T will be replaced by "string".// Type of str is "string"let str = identity<string>("2");// T will be replaced by "boolean".// Type of bool is "boolean"let bool = identity<boolean>(false);
And also, you can drop this "type application", because Hegel will infer which type you want to use.
// "T" is type variablefunction identity<T>(value: T): T {return value;}// T will be replaced by type of 2.// Type of num is "number"let num = identity(2);// T will be replaced by type of "2"// Type of str is "string"let str = identity("2");// T will be replaced by type of false// Type of bool is "boolean"let bool = identity(false);
Generics can be used within functions, function types, classes and type aliases.
type Response<Body> = { status: 200; body: Body };function respondWith<Body>(body: Body): Response<Body> {return { status: 200, body };}// Type of response1 is "Response<{ message: 'Good response' }>"// is the same as "{ status: 200, body: { message: "Good response " } }"const response1 = respondWith({ message: "Good response" });// Type of response2 is "Response<[1, 2, 3]>"// is the same as "{ status: 200, body: [1, 2, 3] }"const response2 = respondWith([1, 2, 3]);// Type of bodyOfResponse2 is [1, 2, 3]"const bodyOfResponse2 = response2.body;
Generic Syntax
As was mentioned before, generics can be used within functions, function types, classes and type aliases. So, there are different syntaxes in different places.
Functions
To define generic parameters for a function you need to add a sequence of needed type variables wrapped in <
>
(angle brackets) separated by ,
(comma) before arguments list
// Function Declaration Generic Syntaxfunction getResponseBodyAndStatus<Status, Body>(response: {status: Status;body: Body;}): [Status, Body] {return [response.status, response.body];}// Function Expression Generic Syntaxconst getResponseBodyAndStatus = function <Status, Body>(response: {status: Status;body: Body;}): [Status, Body] {return [response.status, response.body];};// Arrow Function Expression Generic Syntaxconst getResponseBodyAndStatus = <Status, Body>(response: {status: Status;body: Body;}): [Status, Body] => [response.status, response.body];const obj = {// Method Generic SyntaxgetResponseBodyAndStatus<Status, Body>(response: {status: Status;body: Body;}): [Status, Body] {return [response.status, response.body];},};
Function Type
Function type has the same syntax as for "Arrow Function Expression" with generic:
// Function Type Generic Syntaxconst getResponseBodyAndStatus: <Status, Body>({status: Status,body: Body,}) => [Status, Body] = function (response) {return [response.status, response.body];};
Classes
To define generic parameters for class you need to add sequence of needed type variables wrapped in <
>
(angle brackets) separated by ,
(comma) after class identifier
class Container<T> {value: T;constructor(value: T) {this.value = value;}}let value = 2;// Explicit type application// container1 type is Container<number>const container1 = new Container<number>(value);// Implicit type application// container2 type is Container<number>const container2 = new Container(value);
Type Alias
To define generic parameters for type aliases you need to add a sequence of needed type variables wrapped in <
>
(angle brackets) separated by ,
(comma) after type alias identifier.
type Container<T> = { value: T };const container: Container<number> = { value: 2 };
Type Checking
First of all, if you defined some type as generic you can't use this type without type application.
class Container<T> {value: T;constructor(value: T) {this.value = value;}}const container1: Container<number> = new Container(2); // 👌!// Error: Generic type "Container<T>" should be used with type parameters!const container2: Container = new Container(2);
type Container<T> = { value: T };const container1: Container<number> = { value: 2 }; // 👌!// Error: Generic type "Container<T>" should be used with type parameters!const container2: Container = { value: 4 };
As you may understand, none value will be valid for "type variable". Only arguments which annotated as "type variable" will be a valid value for this type.
function getResponseBodyAndStatus<Status, Body>(response: {status: Status;body: Body;}): [Status, Body] {// Error: Type "[Status, 'Custom Body']" is incompatible with type "[Status, Body]"return [response.status, "Custom Body"];}
Also, the same as Unknown Type you can't get properties from "type variable", because you can be replaced by "undefined", "null" or object which doesn't contain this property.
function length<T>(somethingWithLength: T) {// Error: Property "length" does not exist in "T"return somethingWithLength.length;}
Constraints
But sometimes you need to annotate that your "type variable" can be only subtype of some existed in Hegel type. In Hegel you can annotate this super type after :
(colon).
function length<T: { length: number, ... }>(somethingWithLength: T) {return somethingWithLength.length; // 👌!}let result = 0;result = length([1, 2, 3]); // 👌!result = length({ length: 4 }); // 👌!result = length(() => 2); // 👌!// Error: Parameter "Set<number>" is incompatible with restriction "{ length: number, ... }"// Because Set, WeakSet, Map and WeakMap has "size" property instead "length"result = length(new Set<number>());
Also, it works for primitive types
function plus<T: number | bigint>(a: T, b: T): T {return a + b;}let result: bigint | number = 0;result = plus(1, 2); // 👌!result = plus(1n, 2n); // 👌!// Error: Parameter "'1'" is incompatible with restriction "bigint | number"result = plus("1", "2");// Error: Type "2n" is incompatible with type "number"result = plus(1, 2n);
Default Type
You can also provide defaults for "type variable" the same as for a function argument.
type Container<T = unknown> = { value: T };// container type is "Container<unknown>"const container: Container<> = { value: "something" };