The source code for this blog is available on GitHub.
5 minute read
This post is totally work in progress, and I will keep updating it in the future. It's primarily made for developers who already have a fairly solid JavaScript understanding. Whilst TypeScript is brilliant, it's not amazing for those just starting out. I would recommend getting a solid grasp on the fundamentals of JS before moving on to an abstraction such as TS.
TypeScript brings static types, an overwhelmingly welcoming community, a massively improved developer experience, modern sugar syntax, decorators and much, much more to JavaScript. It does this through an easy-to-learn syntax extension which we will go through today.
Static types allow us as developers to guarantee the type of a variable or argument at runtime. Previously with JavaScript, this was not possible. For example, if we had a function that added two numbers together, we could write this in JavaScript like
function add(a, b) { return a + b; } const result = add(10, 20); // => 30
Now, this is perfectly fine as long as we always pass this function two numbers, but what if we passed it a string instead?
If we did add("Hello", 2)
, it would return "Hello2"
. This is because JavaScript, under the hood, coerces the type of "2" to become a string so that it can be concatenated to the end of "Hello"
. Phew, that was a mouthful.
The issue here is that this function is only meant to add two numbers together, yet we are able to pass other types of variables, and even worse, it "works."
This is where TypeScript comes into play. With TS, we can write this function as
function add(a: number, b: number) { return a + b; } const resultA = add(10, 20); const resultB = add("Hello", 2);
If you ran that in the TypeScript playground, you'll notice a line appear under "Hello"
. It might say something like
Argument of type 'string' is not assignable to parameter of type 'number'.(2345)
TypeScript is telling us that we, as a developer, cannot give this function a string. It must be a number. Now, for a function like add
ing, this might not be very useful, but in a large team you are able to read code that other developers have written very easily. You can see what a function returns, what parameters it takes, the structure of objects, and much more.
For example, if we had a function like this:
function badlyNamedFunctionThatDoesSomethingREALLYComplex( userId: string ): Promise<User> { // ... }
We are able to see exactly what this function returns (a Promise fulfilled with a User type – we'll come on to those shortly) and what arguments it takes. Imagine if this function did not have these type annotations, and we had to work out by running our code multiple times. TypeScript is shortening the feedback cycle for development; I don't have to leave my IDE to have instant information about if my code will run or not.
In TypeScript, we can define the shape of something with an interface or a type. Types can do more than interfaces, but interfaces are nicer for data structures. Here's an example
interface User { name: string; username: string; links: { github: string; }; }
So, you can see here that we have defined an interface called User
(common practice is to name them with a capital letter, like a class). Now, the "complex" function we wrote above is properly typed, as we have created its return type (Promise<User>
).
This is useful for development, as when we are typing in an IDE that can understand TypeScript, it will try to autofill properties and methods of this object.
Types are a great way to represent data that can be an object, like interfaces, but it also supports doing things inline, too. It's more flexible.
We can define a type as simple as:
type UserLinks = { github: string; };
Or, do something more complex, like this:
// Don't worry about what this does for now. // We'll come on to it later. type PartialPick<T, K extends keyof T> = { [P in K]?: T[P]; };
Whilst it's possible to write a PartialPick
type with interfaces, it's much harder and less readable, so you can see when and where to use interfaces vs types.
In the above codeblock, we use something called Generics. They are in many strongly typed languages, including TypeScript. Whilst they look extremely overwhelming and complex, they can be really easily broken down. I learnt them by thinking of them like a function paramater for this type. For example:
// Create "param" // | Add age // | | /-- of type `number` type WithAge<T> = T & { age: number }; type UserWithAge = WithAge<User>;
Now, UserWithAge
is a type that has the same properties we defined above, in User
, but it also has age: number
. The reason this is called a generic, is because we can throw anything into it, and it will always add age
to it.
Commonly people ask me what T
means in this context. It's half just what "everybody else does" but also half because it stands for Type. If we go back to our PartialPick
example, we can see that we define two "params" which are T
and K
. T means "Type" and K means "Key"
However, K
is not just any key – it's a key that extends the keyof T
. So, it's a union of all the keys of T.
If we did
type MyDemoType = PartialPick<{ name: string; age: number }, ?>;
Then, the second "param" (I've left it as ?
for now) we pass to PartialPick
has to extend name | age
.
extend
keyword?Whilst the core TypeScript team have admitted this is not brilliant syntax, the extend
keyword tells the developer that a type must have the base properties or values specified. Similar to extending a class in JavaScript, you copy the base properties and methods of the class you are extending.
A union type is that of two or more types together, for example
type ToastPositions = "top" | "bottom" | "left" | "right";
This is more specific than just string
, as we can have the individual values that our code can use. On a note about extending, this union type we just defined also extends string
!
That is all I have time for now, I will keep updating this post as time goes on. Thanks for reading!