Starting a new job pretty much always means learning something new. With my current job, one of the first new things I had to learn was Typescript. I know my JavaScript decently well, but had never worked with any strictly typed language before, so working with Typescript required more than just learning the types - it required a bit of a perspective adjustment.
I wanted to make sure that I had at least a basic understanding of Typescript before my first day of work, so I started reading about Typescript and even did a short to-do app tutorial. The basic concepts that I wanted to understand were declaring a variable, interfaces, and how types relate to functions. Reading helped me learn, but I found that as I've worked with Typescript, the hands-on experience has really solidified my knowledge.
Most of the work I do involves already created types, and while I do learn a lot from that, I wanted to take some time to focus on the basics. With that in mind, I decided to create something involving from start to finish, sharing my thoughts and knowledge of Typescript along the way.
Declaring A Variable
Declaring a variable in Typescript is not significantly more difficult than declaring a variable in JavaScript. For this article, we'll be creating a pet store, and our first step is to name the store. In JavaScript, we would just declare the name
variable and assign it to the name we wanted to use:
let name = "Furry Friends";
In Typescript, we do almost the same thing, except that we include the type, which goes after the name of the variable (separated by a colon):
let name: string = "Furry Friends";
"String" is just one of the 12 basic types in Typescript. The remaining 11 are boolean (true/false), number, array (which is used together with another type, as explained below), tuple (an array of two elements with mixed types), enum (which allows you to declare a set of named constants - more on that later), any (which, as it sounds, could be any type), void (no type - this is often used as the return type of a function that does not return anything), null, undefined, never (a variable that can never occur), and object.
Interfaces
While it took me some time to get used to actually adding the types, the idea of assigning types to variables wasn't that difficult to understand (although it did require some foresight and understanding of the purpose of variables). But that also wasn't where my adventures with Typescript ended. One concept I needed to become familiar with was interfaces. I haven't found a definition of an interface that I love, but I like to think of an interface as a collection of types that go together.
Let's talk about the pet store we named earlier. As you might expect, our pet store will have pets in it. Let's start with a cat. Our cat will have a name, a color, an age, and an optional parameter, likesYarn (which, as you'd expect, tells us whether or not the cat likes to play with yarn). To label a property as optional, we add a question mark after the name.
interface Cat {
name: string;
color: string;
age: number;
likesYarn?: boolean;
}
To create a cat, we just declare a variable with the type "Cat" with the properties we declared in our interface:
const mittens: Cat = {
name: 'Mittens',
color: 'Gray',
age: 1
}
We labeled "likesYarn" as optional, so we can choose not to define whether or not our cat likes yarn. If we want to create another cat, Fluffy, who likes playing with yarn, we just add in the optional variable:
const fluffy: Cat = {
name: 'Fluffy',
color: 'Brown',
age: 2,
likesYarn: true
}
So now we know how to create a typed variable, and how to create an interface that has typed properties. But - and just hear me out here for a second - what if we wanted an interface where one of the properties was an interface? For our example, we've been creating a pet store. We named our pet store and created a cat interface. But how do we get our cat into our pet store? It's in interface that's a property of an interface.
For the sake of simplicity, we're going to assume that we also have a Dog interface, with similar properties to our Cat. We have a strange pet store that sells kittens and puppies, but there's only one kiten in the store at a time. However, you can have any number of dogs. We also have a property "start size" which tells us how many animals are in our pet store on opening day.
Here's a possible interface for our pet store:
interface PetStore {
name: string;
kitten: Cat;
puppies: Dog[];
startSize: number;
}
But wait - what are those square brackets after the "Dog" interface? As I mentioned above, when we have an array type, we use that in conjunction with the type of the elements in the array. The way this is indicated is by giving the type, followed by square brackets to indicate that it's an array of that type. For example, an array of strings would be string[]
and an array of Dogs would be Dog[]
... as you see above. You can also have arrays with mixed types, but that's not relevant to our current example.
One type that can get a little tricky is enums. Enums are essentially a collection of constants. For our example, lets say that one of the properties of our Dog class is the sound the dog makes most often. There are only a few noises that a dog can make (at least the dogs that our pet store sells), so we can define an enum with these sounds:
enum DogSound {
yip: 'YIP',
bark: 'BARK',
growl: 'GROWL'
}
You might be wondering why we would make an enum if we're just setting the property to a string. Well, I don't know about you, but sometimes when I type, "bark" becomes "baek" and growl becomes "frowl" (in fact, that happened while I was typing this sentence). Using enums ensures that we are using the correct strings and eliminates the likelihood of bugs due to typos. Enums are not possible for all situations, because sometimes you don't know all the possible values, but they can be very helpful if you know that you're choosing from a set number of possible values.
To see this enum in action, let's build our Dog
interface:
interface Dog {
name: string;
age: number;
sound: DogSound;
likesBones?: boolean
}
When we're creating an individual dog, we would use the enum in setting the sound
property:
const Rudy: Dog = {
name: 'Rudy',
age: 3,
sound: DogSound.bark,
likesBones: true
}
When our Typescript compiles, the sound assigned to Rudy will be "BARK".
Now that we've discussed types, interfaces, and enums, it's time to apply these concepts using functions. A function can take in typed parameters and return a type or interface. For example, if we wanted to do a basic addition function, we could do something like:
const add: number = (x: number, y: number) => x + y
This function takes in two parameters, x and y, both of which are numbers, and is expected to return a number. If you were to try to return a string, you would see an error in your IDE (assuming you were using an IDE with type checking) and your Typescript would not compile.
Functions can also be a bit more complicated. Take, for example, our pet store. To create a pet store, we need to run the createPetStore
function, which takes in the name of the store, a kitten, and an array of puppies, and returns our pet store:
const createPetStore: PetStore = (storeName: string, kitten: Cat, puppies: Dog[]) => {
const startSize = puppies.length + 1
return {
name: storeName,
kitten,
puppies,
startSize
}
}
To create the pet store we've been working on, we run it with our store name, one of our cats, and an array of dogs:
const ourStore = createPetStore(name, mittens, [Rudy,Fido,Scooby])
This would give us a pet store with the name "Furry Friends", mittens the kitten, three puppies - Rudy (who we created above), Fido, and Scooby, and a start size of 4. With that, we've officially created a pet store using Typescript. Go us!
This article would not be complete without a discussion of my struggles in learning Typescript. Many of my struggles came from the fact that I was starting with an existing codebase that had a lot of complex types and interfaces. I often did not know what type was assigned to the data I was working with, and I had to trace data very far back to get the right type - winding up with a lot of errors throughout the process. One strategy I developed was to use the any
type while I was brainstorming and planning my work, and then find the right type once I had a better idea what I was working with.
Another area where I struggle with typescript is figuring out what type to use when creating something new. Strings, numbers, and booleans are fairly straightforward, but what if I have multiple variables that are related - should I declare them separately or create an interface? Use an existing interface and just adjust or extend it? Typing variables requires a deep understanding of the purpose of a variable, and when you're working with an existing codebase, sometimes you just don't have that knowledge. I'm definitely getting better at this, but I've certainly had situations where a teammate suggested a different type for a variable I had created.
The Typescript codebase I work with is very complex, and it's been a struggle to adjust. I still make a lot of typescript mistakes, and I know that I will continue to make mistakes, even as I become more comfortable with Typescript. But I wanted to try to break things down a bit and work on a basic-but-still-a-little-complex example in the hopes that it would give me a deeper understanding of how Typescript is used. I'm hoping that writing this post helped solidify my knowledge of Typescript and that my explanations have helped others get a deeper understanding of Typescript and how to become comfortable with a strictly typed flavor of JavaScript.
Top comments (0)