TypeScript is a natural extension of JavaScript that’s used in many projects in place of JavaScript.
However, not everyone knows how it actually works.
In this article, we’ll look at how to extend generic collections and index types in TypeScript.
Using Generic Collections
TypeScript provides generic versions of various JavaScript collections.
They take type parameters to restrict the types of data that can be populated with them.
Map<K, V>
is a JavaScript map with keys restricted to type K
and values restricted to type V
.
ReadonlyMap<K, V>
a map that can’t be modified.
Set<T>
is a set whose value type is T
.
ReadonlySet<T>
is a set that can’t be modified.
For instance, we can define a map by writing:
const map: Map<string, number> = new Map([["foo", 1], ["bar", 2]]);
We pass in strings and values and will get errors from the TypeScript compiler if we don’t.
Generic Iterators
TypeScript also provides us with generic versions of iterators.
Iterator<T>
is an interface that describes an iterator whose next
method returns IteratorResult<T>
objects.
IteratorResult<T>
describes a result produced by an iterator with done
and value
properties.
Iterable<T>
defines an object that has a Symbol.iterator
property and supports iteration,
IterableIterator<T>
an interface that combines Iterator<T>
and Iterable<T>
interfaces to describe an object with the Symbol.iterator
property and has the next
and result
property.
For instance, we can write:
function* gen() {
yield 1;
yield 2;
}
const iterator: Iterator<number> = gen();
We restrict the iterator to only return numbers sequentially.
Also, we can create an iterable object as follows:
const obj = {
*[Symbol.iterator]() {
yield 1;
yield 2;
}
};
const iteratable: Iterable<number> = obj;
Then we can use it with the spread operator or the for-of loop like any other iterable object.
Creating an Iterable Class
We can create an iterable class as we did with iterable objects.
It has the Symbol.iterator
method like the iterable object that we saw before.
For instance, we can write:
class GenericIterable<T> implements Iterable<T> {
items: T[] = [];
constructor(...items: T[]) {
this.items = items;
}
*[Symbol.iterator]() {
for (const i of this.items) {
yield i;
}
}
}
We implemented the Iterable<T>
interface with our own iterable class.
As long as it has the Symbo.iterator
method and it’s a generator function, then it implemented the interface correctly.
Then we can use it by writing:
const itr: GenericIterable<number> = new GenericIterable<number>(1, 2, 3);
Index Types
TypeScript have index types, which we can use to restrict the values to the keys of another object.
For instance, we can write:
function getProp<T, K extends keyof T>(item: T, keyname: K) {
console.log(item[keyname]);
}
Then K
is restricted to be the key of whatever has the T
type with the keyof
keyword.
Then we can use it by writing:
interface Person {
name: string;
}
const person: Person = { name: "joe" };
getProp(person, "name");
We have the Person
interface with the name
property, and then we can call getProp
with the object created with the Person
interface and the string 'name'
.
If we replace the string in the 2nd argument with anything that’s not in the interface, we’ll get an error.
We can add the type parameters.
For instance, we can write:
getProp<Person, "name">(person, "name");
That works the same as we did without it.
However, we can be more restrictive with the types.
Indexed Access Operator
We can use the keyof
operator in an index.
Then we can get the types of all properties and put it into one type alias as a union.
For instance, if we have the interface Person
:
interface Person {
name: string;
age: number;
}
Then if we write:
type allTypes = Person[keyof Person];
Then allTypes
is string | number
.
Then we can write:
const foo: allTypes = 1;
const bar: allTypes = "foo";
As we can see, we can assign a string or a number.
Conclusion
Generic collection types are built into TypeScript.
There are types for iterables, iterators, maps, sets, and more.
There are also types for getting keys of members of other types.