Libraries such as RxJS use generics heavily in their definition files to describe how types flow through different interfaces and function calls. We can provide our own type information when we create Observables to enable all of the auto-complete & type-safety features that you would expect from Typescript. This can be achieved with minimal annotations thanks to the power of generics.
Generics allow types to flow through your program. It's one of the most important things you can learn about TypeScript. Libraries such as RxJS use generics heavily, let's look at how they do it, but more importantly, why?
Let's create an observable that will produce a value every second. Then, if we subscribe to that and try to log it to the console, you can see in the overlay there that TypeScript knows that parameter X here is a number. How exactly does it know that? It's not like we've provided any type information here.
For example, if we did this, then we would expect TypeScript to know it's a number, because we've told it so. How is this working? It's not some kind of magic, it's simply due to the power of generics, but to fully understand this, we're going to have to dive into the type definition files that RxJS ships with.
If we search for interval, we can see that the first parameter should be a number like this, which is exactly how we called it. But the important part is the return type, here, we return observable of type number.
Let's just copy this line into our code so we can start to understand this. This line here returns an observable of type number. Then, we call subscribe on this observable, how does this number get into this subscribe method?
Let's go back to the definition file, and this time search for subscribe. Here is the observable interface, we know it's a generic interface by the use of these two brackets and the capital T here.
You can think of the T here as simply a placeholder for a type. In our case, we said we were returning an observable of type number so that now any methods that are designed inside this block can re-use this type in their signatures, then, it all starts coming together.
If you look at the second definition of the subscribe method, which is the one we're using, in which we pass a fallback function as the first parameter, this is the onNext method, you can see when that callback is called, it will receive a single parameter which is of type T.
This is what I mean, why I say that the types flow through the program? Because the observable interface has declared itself as generic, it takes a single generic type, that allows that type to be used throughout all of its methods. That's how TypeScript can give us type information on this parameter without any annotations.
How can we get the same type safety with observables that we create ourselves? First, let's create an observable as you would if you were wrapping some kind of external service. We use the create method, that takes a function that has an observer, and we'll just produce a single value from this stream.
The value will just be an object literal with two properties, "path," which will be a string, and "event," which will also be a string.
Now, if we subscribe to this, and then try to log it to the console, when we type x., you see we get no type information, no auto-complete, even though we know for a fact that this is the object literal that's coming through and it will have the path and event properties. Now, it's up to us to teach TypeScript about the shape of this object literal.
Let's first model it with an interface, and we'll call it "fileEvent." It has a path which is a string, and we'll say that the event can be either add or change. Now, to use the type information from this interface, we could actually put it on the parameter here. This will give you the auto-complete and type checking that you're expecting.
The problem here is that if you have multiple subscribers you don't want to have to keep repeating this annotation everywhere in your code, let's find a better place for this. We could actually annotate the object literal directly, like this, what's interesting about this technique is that it will provide type checking on this object literal.
For example, we would have to provide either add or change as a string here, that works, but you can see that even when this is correct, we still don't get the type information on this down here. This isn't appropriate, and this is where we can utilize generics. We can actually do it in two ways here.
First, the observer that is given here is actually itself a generic interface, if we go back to the type definitions and search for observer, you can see that if a type is given when it's used, that type flows through this interface into the onNext method as you see here. We can annotate this parameter to say that it's an Rx observer of type fileEvent.
There we have the best of both worlds. We have type checking on this method call, and the type also flows through the system so that we get auto-complete and type checking on properties within the subscribe method.
That's one way to use generics to solve this problem, but in this case, what we should really do is provide it at the highest level you can. In this case, you can provide the type information directly when you call the create method.
We can put here "fileEvent," and that will work. We have a single annotation here, a single interface, for the amount of help that this gives us is incredible.
We can never make a typo in here, or send the wrong string value, and because we're utilizing generics, we can continue to get the same help even later on in our code in different methods without providing any extra annotations.
To recap, create is a generic function which means we can pass a type when we call it. Then, thanks to the type definitions that are provided by the RxJS library, this type can now flow into the observer as you can see here, it enables type safety on the onNext method, and even makes it all the way through to the subscribe method.