- We have finally come
to the most complicated part of TypeScript,
but honestly something
that can be incredibly useful in your code.
Take for example, this really simple code
where we're trying to select an input element
that has the class of input
and then we're trying to get the value of that input.
Well, TypeScript is throwing an error here
essentially saying the "Property value
does not exist on the type of Element"
and that's because query selector
returns an element object to you,
or null if it can't find anything,
which is why I'm making sure to check here for null first.
But the problem is, is it's just given me
the most generic HTML element that's possibly imaginable.
It doesn't tell me that this is an input element
which has a value property,
'cause like divs and stuff don't have values.
So in order to tell query selector,
I'm actually getting an input element instead,
you need to pass in these angle brackets,
so put two angle brackets and inside there
you put the type of the generic you wanna pass along.
So in my case, I want to have an HTMLInputElement,
now by doing that I fixed all of my different errors.
This right here where you see this angle bracket syntax,
where you put types inside of it,
this is a generic insight of TypeScript.
Essentially this query selector function is saying,
I can accept multiple different types of things
or return multiple different types of things
and in order to figure out
what I'm returning or what I'm accepting,
you, the developer, need to pass it along extra information
in the form of these generic types.
You're saying, "Hey, I'm trying to get an element.
And the type of that element is an HTML input element."
Now it's a little bit difficult to really see
exactly how this is working
until you actually start to write your own generics though.
So let's practice writing a generic.
We're gonna create a really simple function
that's just going to return the second element
inside of an array.
So we'll say getSecond, it's gonna take in an array
and then all we wanna do is just return the second element,
which is index one like that.
Now let's create an array,
we'll say const a equals 1, 2, 3,
and what I wanna do is I want to be able to pass this a
to getSecond so we can pass that array inside of there
and we'll just get the value from that, just like that.
So now what we need to do
is we need to type this array object.
Well what I could do is I could type it
as an array of numbers.
So I could say we have a number array
and everything works fine.
You can see this return value is going to be a number,
which is what I expect, and this is passing in just fine.
But now what happens if I create another array?
We'll just call this one b
and this one is gonna be an array of strings instead.
So we'll just put a couple strings inside of here.
And now I want to do this exact same thing,
but I wanna pass in b instead.
So we have our return from b and a return from a,
well now I'm obviously getting a problem
because this is a string array
and I'm trying to accept a number array up here.
So you may think, "Okay, well you know what,
I'll go ahead and I'll add string into here
as another one of my types
and now everything's gonna work fine,"
but my return values are essentially a string or a number,
even though I know in the case of a, it should be a number
and in the case of b, it should be a string,
so that doesn't work,
and if I wanted to try to add
a new array that uses Booleans,
well obviously I need to infinitely expand this.
You may think, "Okay,
well I'll just make this in any array,"
but again, we have another problem
'cause now our return values are all set to any.
So there's really no way to do this
with the knowledge we have,
except for this is where generics come in.
Generics allow you to essentially say
that this could be any type at all,
but that type is going to be something you pass in
when you call the function or when you declare the type.
When you create a function that you want to be generic,
what you do is you put those same angle brackets in there
right after the function name and inside of here
is where you declare all your different generics.
Function can have as many generics as you want,
it doesn't matter.
But what you do is you just declare them
just like you would like the name of a type.
That's how you think of a generic.
It's essentially a type that you're passing in.
Oftentimes you'll see this as T,
but in our case we can be a little bit more specific
where we can say that this is going to be our ArrayType,
because that's what this thing specifically is.
Now we have this type that we can pass in
and we can use this type anywhere inside of our function.
So for example, when we declare our array,
we can say it is going to be of the type ArrayType.
Just by doing that alone,
we've actually fixed all of our code.
You can see here this return value is set to a number
and this return value down here for b is set to a string.
And that's because what I'm calling
this get second function, if I look at it,
you can see that it's automatically inferring
that this type right here for my array type is a number
and my array is a number array,
while when I call it with a string,
you can see it's automatically inferring
this string value as well.
If you wanted, you could manually pass this in
by putting the angle brackets and saying
that this is going to be a number
and this one is going to be a string.
But oftentimes when you're using generics in TypeScript,
it's smart enough to look at the values
you pass into your parameters to actually determine
what that generic is and you may never even need to pass in
the actual generic value here, which is really nice.
So I know this is a lot to digest right here.
So I'm gonna do a quick recap
before we start moving on
to some more advanced concepts for generics.
A generic can be used anywhere you want,
whether it's a type and interface or a function.
In our case, we're specifically using it in a function.
And if you want to declare your function
as a generic function,
you need to make sure
directly after the name of the function,
you put the angle brackets
and then you put all of your generics that you want.
Again, we can have multiple types inside of here if we want
and we can call them anything we want.
Generally you like to use the same notation
as you use with types, so CamelCase capitalized,
but you can put as many as you want or as few as you want.
In our case, we're using just one single generic.
Then these generic values you can use anywhere
you actually define your types for your function.
So I can use them here in the parameters
or I could use it in the return type,
'cause I know this is going to return
whatever my array type is.
So you can use it anywhere you would use a normal type
inside of your function.
But the nice thing is is that this generic type
can be anything, and when you call the function,
you either explicitly pass along
what the generic is going to be,
in our case we pass along number
and now essentially you have the exact same thing,
but every word that you have array type,
it's just replaced with the word number.
That's essentially what happens when you use a generic.
It's just replacing all the places
where you have that generic type
with the specific type that you pass in.
Now the other way you can use these generics
is by not passing in the generic
and let it infer the generic if it is possible.
In that case, you can see here we're passing a string array.
So it knows that this array type should be a string.
- And it just replaces all use cases of array type
with the string parameter here to give you
essentially a version of the function
that only works with strings instead of a generic version.
Now, generics are something
that are fairly common in TypeScript,
but mostly because you're going to be using them
and not so much because you're going to be writing them.
A lot of the generics that you use
are already built into JavaScript
or the libraries you use
and you probably won't have to write too many of them.
So knowing how to use them is more important
than knowing how to write them.
Another really great example of a generic
is the map type or the set type inside of JavaScript.
Let's just use sets 'cause it's a little bit simpler
and let's just say here that const a is equal to a new set.
Now if I hover over this, you can see it's a set of unknown,
we'll get to what unknown is later,
but if I wanted to give this set a specific type,
I could say, you know what?
This is a set of strings, because set takes in a generic
and now I have this a,
which is a set based on string values.
So if I come in here and I want to add to this,
I can add in strings and that's gonna be working fine.
But as soon as I try to add in something
that's not a string,
you see I'm going to be getting an error.
And if I were to pass this a default value,
for example, I passed it a default string,
you can see it's smart enough to actually infer
that the type is going to be string
based on that default value I passed in.
So this is another really great use case
for where you're going to be using a generic type.
Same thing with maps except for maps are a little bit
different because it actually accepts multiple generics.
As you can see here, it's going to accept in a generic
for the key as well as the value.
So I could say this is going to be a key of string
and a value of a number just like that,
and now if I want to add values to this
by coming into here and I want to say set,
I'm gonna set a key here which is a string
to a value that is a number and that works.
But as soon as I try to change something,
you know like obviously this is not gonna work
'cause that's not a string.
And again, if I were to pass it
some default values inside of here,
you can see that it is smart enough
to actually infer all this information for me,
'cause when I hover over this you can see
it has that string number, generic type already inferred.
Now let's talk about how we can actually set this
inside of types as well.
So let's type a type called an APIResponse, there we go,
and this type is going to have some data
which is going to be some type of object,
I don't really know what it's going to be yet,
and then it's going to have an optional error
and this error is just going to be a Boolean,
actually, so let's just change this like this,
we'll say isError, there we go.
So this is gonna be true or false,
whether or not there's an error
and then data is going to return whatever my data is.
Now this is another great use case for generics
because this data could be a lot of different types.
Maybe I'm returning users,
maybe I'm returning posts from a blog,
I don't really know what this is gonna be.
I'm gonna set this to a generic type.
Oftentimes when you're using generics
you'll see the word T being used
in place of an actual name,
or you can get more specific by calling this like TData.
A lot of times you'll see things prefixed with a T
to really denote that this is a type,
instead of saying DataType like this,
it's just a really common thing in generics.
So now to actually make a type or an interface use generics,
all you do is write after the name of the type or interface,
use your angle brackets and put in the name
of the thing you want, tn our case this TData.
Now if I want to use this API response
by saying that a is going to be a type of API response,
I can come in here and I can say
that we're gonna have some data which is an object,
and then we're gonna have, this isError,
which we're gonna set to false, there we go.
I'm obviously getting into some errors
because I'm not passing along my generic type,
so what we could do is we could manually
pass in the type that we want here.
Or in my case I'm gonna create a brand new type.
And this type right here is going to be for UserResponse.
There we go.
And this has some data that's going to be passed into here,
as well as that isError,
but instead of manually defining that,
I can just say that this is going to be equal
to essentially my API response.
But the actual data inside of it is just going to be a user.
So it's gonna have a name which is a string
and an age, which is a number, just like that.
So now this user response extends, essentially,
this API response by overriding what this TData is.
So I have a data and an error,
but this data is specifically this type right here.
So now if I say I have a user response,
it's essentially the same exact code as this right here,
but now you can see my data
requires a name and an age property.
So if I pass in a name and an age,
you can see that now it is working just like I expect it to.
And this is really great
because now if I want to have an API response,
that's going to be for example, a blog instead.
Well now I can change this to be like title
and this, you know, could be all there is for my blog,
and now I can come down and I can create something brand new
that's going to be for my blog response, change this to b,
and now my data requires that title property here
instead of the name and the age,
as you can see, no more errors at all.
So this is just like using the generics with a function,
but instead we're now using it in a type.
And if I wanted, instead of extracting this user response
into its own type, I could just replace this down here
and it's going to work exactly the same,
using this extraction into another type
just makes it a little bit easier to read sometimes
and makes it more reusable.
Now what happens though,
let's just bring this back to how we had it before,
if I wanted to specify a default value for my response,
because let's say, you know,
not all of my stuff has some data
and by default it'll just have a status
and maybe a lot of my responses have that default value.
What I can do is I can say that by default
this is equal to a specific value.
So I can just say by default
this is equal to a status with a number property.
So by default, if I don't respond with a generic,
when I define my API response, it's just going to use
this value right here, status of number.
So if I just get rid of all this code,
I'm gonna bring it up here, there we go.
And I'm gonna create a brand new variable
and this is going to be my API response, whoops,
APIResponse, and here we have our data.
And I know by default that if I don't pass along a generic,
it's going to have a status, we'll say 200
and isError is going to be false just like that.
And as you can see, that's working just fine,
but as soon as I pass in a generic type here
of whatever it's going to be,
for example, name string that overrides this default value
and now I need to change this to a name property
which has a string value instead.
This is a great way of doing a default value.
Another thing that you may run into
is that our data we know should always be an object,
at least in this case we're assuming
it's always going to be an object.
So if I try to pass along something like string here,
obviously this is going to work just fine as is,
but I know that this should always be
an object instead of like a string, a number or an array.
So what I can do is I can also say
that this is going to extend a particular value,
for now I'm gonna remove the default value,
so it's a little bit easier to see what's going on,
but I can say this extends object
and now I'm going to get an error,
'cause string does not extend object.
So this must be an object of some form
in order to actually work.
There we go, and now if we bring this back,
you can see we no longer have any errors.
This extend property is really great
if you want to kind of constrain
what your generic is going to be.
'Cause a generic by default can be any type at all,
every type in TypeScript is valid,
but by making it extend other things, you're saying,
you know what, I'm constraining this to only be objects.
Or if I'm saying it extends a string,
it must only be a string in our case,
or maybe you're extending array, it must be an array.
And if we bring this back to where we had object,
I can still use the same default value,
it just goes after my extend.
So I could say status is going to be a number by default.
So now if I don't pass anything in here,
you can see it's going to expect me
to pass along a status, which is a number.
Now I'm gonna take this back down a little more bare bones
where all it is is just a data,
it can be literally anything.
And in our case we'll just come in here,
we'll say status number,
just so our code is actually working, there we go.
Now I want to talk about a built-in type in TypeScript,
which is also a generic,
one that you are somewhat familiar with actually
because it's a second way of defining arrays.
So if we wanted to create an array here,
we could say it's like 1, 2, 3, this is an array of numbers,
make this b here, what I could do
is I could type this as the array type,
which is a generic type
which takes in the type of the array.
This is exactly the same as writing out number array
just like this, they both do the exact same thing
when it comes to creating an array.
So you are kind of already familiar with generics,
we just haven't been writing this as a generic,
but both of those are the exact same thing,
which is really interesting.
So if I wanted my data to be an array of numbers,
for example, 1, 2, 3, like that,
well I could come in here and I can say array of numbers.
Whoops, there we go, just like that.
And I need to make sure I actually have my API response,
so let's just back this up to where that was
before I deleted it by accident, there we go.
We have our API response up here
and I can change this to an array of numbers,
just like that.
And now I can just pass in 1, 2, 3,
and you can see that that works.
So the important thing to note about this
is first of all, there's this built-in type array
which works just like the other array type, as you can see,
both of these pieces of code do the exact same thing.
The second thing to note
is you can actually nest generics inside of each other.
As you can see, we have this array generic,
nested inside of our API response generic,
it works just fine.
A generic is just like any other type in TypeScript,
it just requires you to pass along information
that tells you what the generic type is supposed to be.
That's all a generic really does.
So at this point we understand generics,
but we haven't really used them very much.
So I want to create a function
that's going to heavily use generics
as well as many of the other features we've talked about
inside of this section,
in the last couple sections of the course,
I'm gonna create a function
and this function is just going to take an array
and convert it to an object,
so essentially it's gonna be an array of arrays
and we'll say aToO,
is what we'll call this, array to objects.
It doesn't really matter what the name is.
And the way I want to call this is I want to have an array
and I want this array to be an array of arrays,
so like the first thing is going to be like keyOne
and then we're gonna give it a value,
and then we're gonna say keyTwo,
and we're gonna also give that a specific value,
and we'll come in here, let's say keyThree,
and this is also going to be a value,
in our case, we'll give this one a Boolean value
and make sure that this is inside of an array as well.
Now you can see I have an array with three different arrays
and I want to essentially convert this to an object
that looks like this,
keyOne is 1, keyTwo is 2, and keyThree is true.
That's what I want to convert this to using my function.
This is gonna require generics
it's going to require a bunch of other things
that we've already talked about.
So the first thing we know
that we need to pass into this is some type of array.
And generally, one thing that you can do
if you're not really sure
what the TypeScript code should look like,
especially in more complex functions,
is you can start by writing a lot of your code in JavaScript
and then add the TypeScript in later
if you're a little bit confused
on what the TypeScript should look like.
The JavaScript for this isn't too terribly complicated.
So we're gonna create an object here,
which by default is just gonna be an empty object,
and then we want to return that object at the very end.
Another way we could do this
is with the reduce function as well.
It's entirely up to you which way you wanna go.
In our case, I'm just gonna stick with this object method.
Now what we can do is we can loop through our array.
So we can save for each element in our array.
What I want to do is I know that this element
is specifically going to be multiple parameters.
The very first parameter is going to be my key,
and the second is going to be my value.
So I can say object at this specific key
is going to be equal to that specific value, there we go,
that's all I need to do in my forEach,
and then I'm returning my object down here.
Now I need to add types to make all of this work.
Well, I know that this is going to be an array
if I specify this as an any array,
that at least gets me part of the way there,
but I actually know that this is a tuple,
it's going to be specifically a tuple array.
So I have a tuple array here,
where the first value is always gonna be a string
'cause I need to have a string is my key
and the second value I have set to any.
So that's working really well, and here for my object,
I can just define this as a type of any right now
just to get rid of all those type errors.
So that's one way that I like to take my code
when it's JavaScript and converting it to TypeScript,
throw in any anywhere I don't really know
what the type is going to be,
otherwise manually specify the type.
So here my array is essentially a tuple array
where I know the very first value is a string
and the second value I'm not really sure
what it's supposed to be.
That's a great use case for generics though
because depending on what array I pass in,
I know that this specific value is going to be different.
So we'll just use the letter T
to determine what this is going to be, there we go.
Now I have this generic T type right here
which is going to be what my value is,
you can see it has that specific T type,
and here my key has that specific string type,
which is really great.
Now for defining the type of my object,
I can use that really fancy index syntax
that I had from before, so instead of being an any object,
this can be an object where the index here
is specifically going to be a string.
'cause I know that this is a string type right here
and the value is going to be T.
It's just gonna be one of these different T values.
Now if I save that, you can see that this code right here
is working fine and if we call it,
it should also work as well.
So let's just come down here,
we're gonna say aToO pass in our array
and we're gonna get the value,
which is just our object, out the other end,
and we can see that the type of our object
is an index, which is a string,
and a return value, which is a string, or a number,
but it looks like we have a little bit of a problem going on
with our actual array that's being passed in.
The reason for that is actually because this array here
should probably be typed as a tuple.
So we can type it as a tuple
where the first value here is a string
and the second is either a number or a Boolean,
just like that.
And then if we make sure that this is an array,
there we go, we can see we've gotten rid
of all of our different errors
and if we hover over this object,
you can see that it has this type
where the index is a string
and the value is either going to be a number or a Boolean.
So it's converted our array properly into this object type.
Now I'm gonna really quick refresh what we did
because this is actually fairly advanced TypeScript code.
I guarantee in 99.9% of your days,
you're not gonna write TypeScript code this complicated.
But this is a great example of really trying to figure out
how everything works, 'cause it combines together
a bunch of the different things that we've talked about.
The very first thing we did
is we wrote it as a JavaScript function,
since that's what we're familiar with
and everything was working fine.
Then we started adding in the types
and we knew that the first value in our array
is always gonna be a string
just because that's how our object is set up,
it always must have a key as a string,
and then we know the second value
can literally be anything at all,
so we gave this a generic type of T.
Since, depending on what arrays we pass in,
we know we want this value to be different.
Then we just specified this is our generic T,
and now for our actual return value of an object,
we know we wanna take whatever the first value
of the array is and use that as our index,
that's why we wrote this code right here,
and we know that the value of that is just going to be
whatever the second value of our array is.
That's why the value is set to that T variable here.
The rest of this code is just plain JavaScript,
there's no TypeScript at all,
it just infers everything we need.
Then down here just to make sure
everything is linked up properly,
we're essentially expecting this
to be a tuple to be passed in,
so we need to make sure that this array
was also considered a tuple.
Now, like I've said, this is a very complex
bit of TypeScript code, so if you didn't quite understand
exactly everything that was going on, that's perfectly okay.
But this is a good showcase
of pretty much the most advanced stuff
that you're probably ever going to write using generics
unless you're writing some really complex
library style code.
For the most part though, when you're using generics,
it's gonna be something as simple as you know,
document.querySelector, and you're gonna pass in
that you want it to be an HTMLInputElement
that you're getting from here instead.
That's about as complex
as most of your generics are going to get.