Typescript — Generics, and overloads 👨🔬
This article is part of the collection “TypeScript Essentials”, this is Chapter four.
In the last article “TypeScript — Learn the basics”, we saw all basic usages of TypeScript types.
By the end of this article, you’ll understand why generic types are essential for real-world usage.
Generic — Configurable types
While basic types like
interfaces
are useful to describe data and basic functions signatures, generics helps making types “open” and reusable.
Why use Generic types?
Imagine we want to expose the following helper in our application:
function withUID (obj) {
return Object.assign({}, obj, { uuid: _.uniqueId() });
}
The more straightforward way to “type” this function would be:
function withUID (obj: any) {
return Object.assign({}, obj, { uuid: _.uniqueId() });
}
We use any
because we want to accept any value — indeed we can use object
.
The problem is that the inferred return type of the function is any
.
By using scalar types (object
, …) or any,
we prevent TypeScript to infer the return type.
To overcome this problem, we’re gonna use generics.
⠀
Using Generic types
In languages like C# and Java, one of the main tools in the toolbox for creating reusable components is generics, that is, being able to create a component that can work over a variety of types rather than a single one.
https://www.typescriptlang.org/docs/handbook/generics.html
As explained this excerpt of the TypeScript documentation, we want to create functions that accept many kinds of types, without losing type — without using any
.
Let’s see the withUID
using generics:
function withUID<T>(obj: T) {
return Object.assign({}, obj, { uuid: _.uniqueId() });
}
The <>
syntax is reserved for describing generic type.
A generic expose many “type arguments”, listed between the <>
.
Generics can be applied to interfaces
, class
and function
.
interface A<T, S> {
a: T;
b: S;
c: { id: string } & S;
}
As you can see, we can define as many “type argument” as needed.
If no type argument type is explicitly passed, TypeScript will try to infer them by the values passed to the function arguments.
Example, for withUID
, T
is inferred from the type of obj
argument.
⠀
Generics can “extends”
⠀
The type argument can provide some constraints by using the extends
keyword.
function withUID<T extends object>(obj: T) {
return Object.assign({}, obj, { uuid: _.uniqueId() });
}withUID({ a: 1 }); // is valid
withUID("hello"); // is NOT valid
Here, T
should fulfill the condition “is an object type”.
interface Person { name: string; }function withUID<T extends Person>(obj: T) {
return Object.assign({}, obj, { uuid: _.uniqueId() });
}withUID({ name: "POLY", surname: "Chack" }); // is valid
⠀
Generics can have a “default type value”
Finally, argument types can also have “default” or “computed value”.
interface A<T=string> {
name: T;
}const a:A = { name: "Charly" };
const a:A<number> = { name: 101 };
This is particularly important for interfaces which unlike function generics cannot omit type argument, example:
That’s why “default type value” is a “nice to know”:
⠀
Tips — default type arguments can reuse other type arguments
The example below describe how we can define a type of argument on-top of another:
function MyFunction<T extends Person, S=T&{ ssid: string }>(
person: S
): S {
/* ... */
}
⠀
Overloads — Extendable Function types
⠀
Generics are convenient for the functions that are responsible for simple isolated computation or predictable return value for given a input.
Unfortunately, everything isn’t this simple, some “big” functions may have high complexity with the variant return type.
Generics can be used in combination with “overloads” to overcome this issue.
For example, how can we type this function?
function getArray(...args) {
if (args.length === 1 && typeof args[0] === 'number') {
return new Array(args[0])
} else if (args.length > 1) {
return Array.from(args);
}
}getArray(5) // => [undefined x 5]getArray('a', 'b', 'c') // => ['a', 'b', 'c']
Here is the inferred result type:
The solution is to use “Overloads”.
The answer is to supply multiple function types for the same function as a list of overloads. This list is what the compiler will use to resolve function calls.
https://www.typescriptlang.org/docs/handbook/functions.html
Overloads are the fact of listing all the possible input/output types couple for a given function.
⠀⠀
ℹ️ Please note that :
- overloads have nobody
- the implementation function must have as open as possible typing
(to allow overloading)
Real-world examples
⠀
interface with Generic example: React.Component
You probably know how to declare a React Component in ES6:
class MyComponent extends React.Component {}
With TypeScript, the React.Component
class refer to Component
type.
Component
type extends ComponentLifecycle
— which provide all famous lifecycle methods:
So, if you use React with TypeScript, remember to provide type arguments for Props and State!
interface Props { user: User }
interface State {}class MyComponent extends React.Component<Props, State> {
state: State = {}; // important!
// ...
}
⠀
Overloads example: lodash _.filter()
_.filter
can take different types of arguments as value, which results in a pretty complex mix of Generic with Overloads typing.
All lodash
functions provide this advanced and complete typing!
I hope you now understand the concepts of Generic and Overloads and especially why it’s important is a real-world TypeScript project.
If you want to dive further and level-up your typing skills, I advise you to take a look at the following typings :
- React-Apollo types
- react-redux types: especially for
connect()
which will introduce you to decorators typing.
⠀
The next chapter —“Typescript — Super-types 💪”— will cover more complex cases with more real-world examples 👷
Latest Serie is out!
Interested in React? You should check-out my last publication:
Follow me on Twitter to stay up to date on React, GraphQL, and TypeScript!