You can, of course, also use generics in your class definitions:
class NaiveMap<Key, Value> { private _keys: Key[] = []; private _values: Value[] = []; constructor(){} contains(key: Key): boolean { const result = this._keys.indexOf(key); return result !== -1; } put(key: Key, value: Value): void { if(!this.contains(key)) { this._keys.push(key); this._values.push(value); } } get(key: Key): Value | undefined { if(this.contains(key)) { return this._values[this._keys.indexOf(key)]; } else{ return undefined; } } } class Thing { constructor(public name: string){} } const naiveMap = new NaiveMap<string, Thing>(); naiveMap.put("foo", new Thing("The thing")); console.log(naiveMap.contains("foo")); // true console.log(naiveMap.get("foo")); // Thing { name: 'The thing' }
In this example, we have defined two generic parameters that we use to create generic arrays and define the input and output types of our functions.
As you can see, thanks to generics, this very naive implementation of a Map data structure can be used with many different types and is therefore highly reusable. This also highlights the fact that, in a generic class, you may define generic members as we did previously (that is, the _keys and _values arrays).
One more thing to note is that you can define default generic types for both interfaces and classes. This is especially useful for interfaces because it can allow you to omit generic types. The following is an example:
interface InterfaceWithDefaultGenericType<T=string> { doSomething(arg: T): T } class ClassWithDefaultGenericType<T=string> { constructor(public something: T) {} } interface InterfaceWithSpecializedGenericType<T = Person & {age: number}> { doSomethingElse(arg: T): T }
Next, let's look at generic constraints.