What & Why
Setup
Type Annotations
Type AnnotationsPrimitive TypesUnknown TypeOptional TypesUnion TypesFunction TypesObject TypesClass TypesArray TypesTuple TypesType AliasesGeneric TypesGeneric SyntaxType CheckingConstraintsDefault TypeMagic TypesModules
Type System
ConfigurationLibraries
For Potential Contributors
Index

Generic Types

Edit

Sometimes you can have different types for the same logic. The easiest example of this case is identity function:

Playground
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:

Playground
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.

Playground
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.

Playground
// "T" is type variable
function 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.

Playground
// "T" is type variable
function 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.

Playground
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

Playground
// Function Declaration Generic Syntax
function getResponseBodyAndStatus<Status, Body>(response: {
status: Status;
body: Body;
}): [Status, Body] {
return [response.status, response.body];
}
// Function Expression Generic Syntax
const getResponseBodyAndStatus = function <Status, Body>(response: {
status: Status;
body: Body;
}): [Status, Body] {
return [response.status, response.body];
};
// Arrow Function Expression Generic Syntax
const getResponseBodyAndStatus = <Status, Body>(response: {
status: Status;
body: Body;
}): [Status, Body] => [response.status, response.body];
const obj = {
// Method Generic Syntax
getResponseBodyAndStatus<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:

Playground
// Function Type Generic Syntax
const 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

Playground
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.

Playground
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.

Playground
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);
Playground
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.

Playground
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.

Playground
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).

Playground
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

Playground
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.

Playground
type Container<T = unknown> = { value: T };
// container type is "Container<unknown>"
const container: Container<> = { value: "something" };