DEV Community

Ralf πŸ’₯πŸ”₯β˜„οΈ
Ralf πŸ’₯πŸ”₯β˜„οΈ

Posted on • Originally published at kissdev.io

Generic parameters in typescript for beginners

Generic parameters are a very powerful concept. But they are not easy to understand for beginners. In this article I'm going to explain them in a beginner friendly way.

This article was originally published on https://kissdev.io/blog. You can find a lot of similar content there.

The article will have the following progressions:

  • Non generic class
  • See limitations of non generic class
  • Add generic parameter to class
  • "Realworld" example
Let's start with a simple non generic class to see its limitations:
class Transformer{
    private transformMethod : ((values : string[]) => string[]);
    private values : string[];

    constructor(transformMethod : ((values : string[]) => string[])){
        this.values = [];
        this.transformMethod = transformMethod;
    }

    public add(value : string) : void{
        this.values.push(value)
    }

    public transform() : string[] {
        return this.transformMethod(this.values);
    }
}

The Transformer class can hold any number of strings. You can add new strings to it by calling the 'add' method. When creating a new instance of the class, you can pass a method that transforms all strings that were previously added and returns a new list of strings that contains all transformed strings previously added. This is basically the js map method reimplemented (of course with a lot of functionality missing).

Here is a use case for our transformer class:

var makeUpper = (values: string[]) : string[] => {
    return values.map(v => v.toUpperCase());
}
var upperTransformer = new Transformer(makeUpper);
upperTransformer.Add("i'm all lowercase")
upperTransformer.Add("really sad")
var allUpper = upperTransformer.Transform();

The method makeUpper takes a list of strings and returns a new list of strings with every element uppercased.
We then create a new instance of our Transformer class and pass in the makeUpper method.
We now add some strings and call the transform method. This will give us a new list of strings with all elements uppercased.

Let's have a look at another example:

//This will not work
var addFive = (values: number[]) : number[] => {
    return values.map(v => {return v + 5});
}
var addFiveTransformer = new Transformer(addFive);
addFiveTransformer.Add(1)
addFiveTransformer.Add(2)
var allFiveAdded = addFiveTransformer.Transform();

This time we defined a method that adds 5 to every number in a list and returns a new list with the modified numbers.
This will give us typescript errors because our Transformer class expects to work with strings.

What do we have to do to make our Transformer class work with every datatype?

A generic parameter:

class GenericTransformer<T>{
    private transformMethod : ((values : T[]) => T[]);
    private values : T[];

    constructor(transformMethod : ((values : T[]) => T[])){
        this.values = [];
        this.transformMethod = transformMethod;
    }

    public Add(value : T) : void{
        this.values.push(value)
    }

    public Transform() : T[] {
        return this.transformMethod(this.values);
    }
}

You see the difference?
We removed every occurrence of the type string from the class and replaced it with a so called generic type. We called the generic type T, but we could have called it whatever we want.
T is a placeholder for the type that we specify when we create an instance of the class.
Let's do that now:

var addFive = (values: number[]) : number[] => {
    return values.map(v => {return v + 5});
}
var addFiveTransformer = new GenericTransformer<number>(addFive);
addFiveTransformer.Add(1)
addFiveTransformer.Add(2)
var allFiveAdded = addFiveTransformer.Transform();

You see that we now say that the generic type of our class instance should be of type number. Now typescript knows how to handle the generic parameter. Every occurrence of 'T' will now be an instance of type number.
This time our code compiles. And it will compile for every other datatype too.

Of course this simple example is pretty much useless.

Let's have a look at an example that could be used in a real life application:
class DbConnection{
    public Save(data:any): void{
        //save to db
    }
}

class Repository<T>{
    private dbConnection: DbConnection;

    private data:T[]

    constructor(){
        this.data = [];
        this.dbConnection = new DbConnection()
    }

    public Add(data: T): void{
        this.dbConnection.Save(data);
        this.data.push(data);
    }

    public Get(): T[]{
        return this.data;
    }
}

class Todo{
    public task:string;
    public done:boolean;
}
class Bookmark{
    public url:string;
}

class MyApp{
    private todoRepository: Repository<Todo>;
    private bookmarkRepository: Repository<Bookmark>;

    constructor(){
        this.todoRepository = new Repository<Todo>();
        this.bookmarkRepository = new Repository<Bookmark>();

        var myTodo = new Todo();
        var myBookmark = new Bookmark();

        this.todoRepository.Add(myTodo);
        this.bookmarkRepository.Add(myBookmark);

        var allTodos : Todo[] = this.todoRepository.Get();
        var allBookmarks : Bookmark[] = this.bookmarkRepository.Get();
    }
}

What we're trying to achieve here is creating a Repository that holds data objects and can save data objects to a database. And it has to work with any kind of data object.

First we define a class called DbConnection. It doesn't do anything and just exists to show what a real world repository could use to communicate with a real database.

The Repository class is where the 'magic' happens. But once you understood the concept, it's a pretty simple class isn't it?
It creates a mocked database connection in its constructor and has a method to add new data and a method to return all data. The trick is that the class doesn't care at all about the type of the data. It stores them regardless.

Finally the MyApp class shows how to use the repository class. We create two repositories. One holds data of type Todo and the other of Type Bookmark.
Now both repositories have the same functionality but only work with their respective data type. You can not accidentally add a Bookmark object to the TodoRepository. The typescript compiler will complain about it.

And that's it. We created a fully generic Repository class that can store any kind of data. Pretty cool.

Here is why generic types are so important for kissjs (the javascript framework I'm working on):

A fundamental part of kissjs are so called Business Logic blocs(bloc). These are classes that contain all the business logic your app consists of and are accessible from everywhere in your application.
And there are special kinds of blocs which are responsible to handle data objects. Pretty similar to our Repository example. These DataBlocs can create, read, update and delete data out of the box. They work in the same way as the repository above. When a DataBloc gets created, it gets the type of its data passed as a generic parameter and you as the developer never have to worry about passing wrong data to it.

Kissjs is a javascript framework that makes it very easy to create mobile apps very quickly. If you want to learn more about it, check it out at https://kissdev.io.

And if you are interested in this kind of topics, I share a lot about it on twitter. So make sure to follow me there @RalliPi.

Top comments (0)