Learning TypeScript with React

⚡️TL;DR: Understanding what are types, type annotations, why use them and how to use them can help you catch errors during development while also enhancing code quality and readability.

Before diving into it, let me give you a short definition of what is TypeScript (TS) taken from the official TS website.

“TypeScript is a typed superset of JavaScript that compiles to plain JavaScript.”

Now, let me also give you a high level overview of the app’s features and how the React side is structured.

It is a one page app where it has a video player, a list of related talks and a search bar which are all built as separate React components. On page load, it fetches a default list of talks from a JSON file instead of fetching from Youtube to avoid exceeding the daily usage quota of the Youtube API when the page is loaded by many users. The app only communicates with Youtube when searching for talks.

Okay, let’s start.

First, the project setup

I generated the project using Create React App with TypeScript

create-react-app my-app-name --typescript

How did I use TypeScript in this app?

Since I’m a TS beginner, I get started by learning the syntax and features of the language first specifically the types, type annotations and interfaces. In this first post, I’m gonna be talking about types and annotations only.

What is a type?

Since TypeScript is a typed language, it means we can specify/annotate the type of the variables, function parameters and object properties.

From my own understanding, a type is a symbol/representation of all the properties and methods that a variable has access to. It means if we added a type of number to a variable, the TS compiler knows that the variable has and can only access all the properties and methods that a number has.

A type is annotated to the variable, parameter or property using this format :type.
For example, let name: string.

There are a lot of available types in TS however, in this app, these are the ones that I’ve used:

  1. Primitive Types - number, string, boolean
  2. Arrays - by adding [] after the type (e.g. let names: string[])
  3. Video - an interface or a custom type I created to represent a video object’s properties

See here for a more comprehensive list of types.

Why do we care about annotating types?

  1. Types are one of the best forms of documentation you can have. This is a very helpful to the next developer who has to read your code which could also be the future you.
  2. It helps the TypeScript compiler help you. If you annotate a variable as a number and a few lines later assigned it a value which is a string, the compiler will give you the following error.
let x: number = 123;
x = '123'; // Error: cannot assign a `string` to a `number`

Even if we remove the :number annotation from variable x, the same error will be given by the compiler because TypeScript is smart*.

*If you declare a variable with a value without specifying the type, TypeScript will figure out the type of the variable based on the initial value assigned to it.

This is called type inference.

“So if type inference is present then why do I have to bother annotating the variables?”

Type inference does not work all the time.

An example scenario is when you delay the initialization of a variable as shown below.

let age;

// a few lines later
age = 12; 
// The compiler will NOT give an error even 
// if we assign a non-numeric value to `age`
age = 'Ana'; 

If you delay the variable initialization without annotating a type to it, the variable will automatically have a type of any associated with it since the TS compiler does not have any idea what kind of properties age will have later on.

Variables with a type of any should be avoided as much as possible. It defeats the purpose of the idea behind the TS type system where we want to catch as many errors as possible during development. Why? Because, the compiler cannot do error checking on types of any.

Remember, use types to help the TS compiler help you.

How is type annotation used in the app?

Aside from using types on the variable declarations, I also used annotations in the function declarations.

A function can be annotated in the following ways:

1. Arrow function

const add = (a:number, b:number):number => {
 return a + b;
}

// or
const add: (a: number, b: number) => number = (a, b) => {
  return a + b;
};

2. Non-arrow function

function add(a:number, b:number):number {
 return a + b;
}

In both of these examples, the compiler will give an error if the function returns a string or any type that is not a number since it is expecting a number.

For fetching the channel’s videos, I created a function called fetchChannelVideos which accepts a boolean flag indicating whether to fetch the default videos from the JSON file and a search query string. Both of these are optional parameters (by adding ? after the property name) which are represented as an interface. I will explain later what an interface is but for now let’s take a closer look on how the function is annotated.

interface FetchVideosArguments {
 shouldUseDefaultVideos?: boolean;
 searchQuery?: string;
}

export const fetchChannelVideos: (
 args: FetchVideosArguments
) => Promise < Video[] > = async ({
 shouldUseDefaultVideos = false,
 searchQuery
}) => {};

On the left side of the assignment operator (=),

const fetchChannelVideos: (args: FetchVideosArguments) => Promise <Video[]>

we are annotating the variable fetchChannelVideos that was declared not the function assigned. We are telling the TS compiler that this variable will have a function assigned to it with these types of arguments and return value.

While the right part of the = is the function assigned.

async ({
 shouldUseDefaultVideos = false,
 searchQuery
}) => {};

To annotate the function itself, we have to specify its arguments and their types and the return value as well.

So why didn’t I annotate the function to assigned to fetchChannelVideos? Because again, TypeScript is smart.

Seeing that I assigned the function to a variable that was annotated, it is able to infer that the function will have the same argument names and types and the return value as well, otherwise it will give an error if I add or specify different argument names or return a different value.

*The function arguments and return value is inferred

However, if I’m exporting the function directly without assigning it to a variable then I have to annotate it like below.

export async function fetchChannelVideos({
 shouldUseDefaultVideos = false,
 searchQuery
}: FetchVideosArguments): Promise<Video[]> {
 // function body
}

Okay, now that we have an idea about the what, why and how of type annotations, there’s one last question.

Where do we use types?

The answer is: everywhere. By using TypeScript, every data in your application will have a type associated with it whether you like it or not.

Closing Thoughts

I have only touched the basics so far but I’ve already seen how helpful it is, seeing the errors the compiler gives while writing the code instead of discovering them later when running the app saved me a lot of time.

What is an interface?

Interfaces are the core way in TypeScript to compose multiple type annotations into a single named annotation.

It’s a way of creating a new type that describes an object’s property names and their types. Remember the types that we covered before (number, boolean, string, etc.)? An interface is the same as those types.

Why should you care about interfaces?

For me, interfaces are one of the best features of the TypeScript language. Let me show you just how awesome it is.

Let’s start with the following example.

You have a function called renderVideo which accepts video as a parameter in which you annotated with all the properties that should be present in a video object.

const video = {
  videoId: '',
  title: '',
  description: '',
  thumbnailURL: ''
};

const renderVideo = (video: { videoId: string; title: string; description: string; thumbnailURL: string }): void => {

}

Even though the parameter annotation is quite long, the code above is okay, the function parameter and return value was annotated correctly.

However, imagine if you added more functions which accepts a video.

const video = {
  videoId: '',
  title: '',
  description: '',
  thumbnailURL: ''
};

const renderVideo = (video: { videoId: string; title: string; description: string; thumbnailURL: string }): void => {

}

const getVideoSummary = (video: { videoId: string; title: string; description: string; thumbnailURL: string }): void => {

}

// another function has a `video` parameter
// another one
// and another one
// *shouts* "DJ Khaled"

Aaahhhh. I don’t know about you but to me, the code above doesn’t look good.

There are a lot of long annotations which are duplicated several times. The code is harder to read as well.

Now imagine again (we’re imagining a lot here) that you need to do either one or more of the following changes to a video:

  • add the date it was published as a new property
  • rename thumbnailURL to imageURL
  • change the type of videoId from string to a number

Applying these new changes to the code above means you need to change all the video annotations in a lot of places.

That’s a lot of work and we don’t want that so how can we solve this?

Simple, by using an interface.

How to create an interface?

We can convert this long video annotation into an interface called Video.

To create an interface, use the interface keyword followed by a name that represents the object, in this case Video.

The name should start with a capital letter just like how we create React components and a set of curly braces where you declare all of the object’s properties and methods and its types.

This is how you create the Video interface.

interface Video {
  videoId: string;
  title: string;
  description: string;
  thumbnailURL: string;
}

How do you use an interface?

Since an interface is a custom type, we can annotate it to a variable just like a normal type.

let item: Video;

By using the Video interface, we can rewrite the previous example to this:

interface Video {
  videoId: string;
  title: string;
  description: string;
  thumbnailURL: string;
}

const renderVideo = (video: Video): void => {};
const getVideoSummary = (video: Video): void => {};

// another function that uses a video
// another one
// and another one

const video = {
  videoId: "",
  title: "",
  description: "",
  thumbnailURL: ""
};

renderVideo(video);

This code looks wayyyyy better than the previous one right?

It looks cleaner and in one glance, we can see right away that the parameter is a type of Video.

Most of all, we can add, remove or rename a property or change a property type only in one place, the interface declaration. Awesome!

💡 If you use VS code, which I highly recommend you should, you can see all the property names and types of an interface by holding the ctrl/cmd key and hovering over the interface name.

Pretty cool right?

How are interfaces used in the app?

Aside from using the Video interface, I created another interface to represent how the Youtube API response is structured.

interface YoutubeVideo {
  id: { videoId: string };
  snippet: {
    publishedAt: string;
    title: string;
    description: string;
    thumbnails: { medium: { url: string }; default: { url: string } };
    channelTitle: string;
    channelId: string;
  };
}

interface YoutubeAPIResponse {
  data: { items: YoutubeVideo[] };
}

This interface is used in the fetchChannelVideos function.

export const fetchChannelVideos: (
  args: FetchVideosArguments
) => Promise<Video[]> = async ({
  shouldUseDefaultVideos = false,
  searchQuery
}) => {
 const requestURL = '';
 const response:YoutubeAPIResponse = await axios.get(requestURL);
};

Since an interface is a type, it means the TS compiler also does error-checking on the annotated data.

By creating an interface for the API response structure and annotating it to the variable, we made sure that we don’t accidentally access a property that does not exist.

So if we try to access comments, which is not included in the API response, the TypeScript compiler will give us an error.

Writing Reusable Code with Interfaces

Now that we covered the what, why and how of interfaces, let’s talk about how we can use it to write more reusable code.

Let’s say you have a component that renders comments from a Youtube user and the channel.

First we write it like this:

interface User { name: string; avatar: string; }
interface Channel { name: string; avatar: string; }

const renderUserComment: (user: User, comment: string) => React.ReactNode = (
  user,
  comment
) => { /* return user comment */ };

const user = { name: "Ana", avatar: "ana.png" };
const channel = { name: "Tedx Talks", avatar: "tedxtalks.png" };

const renderChannelComment: (
  channel: Channel,
  comment: string
) => React.ReactNode = (channel, comment) => {/* return channel comment */};

renderUserComment(user, 'hello there');
renderChannelComment(channel, 'hi, thanks for watching');

There are two separate interfaces to represent a user and a channel as well as separate functions to render these comments even though they do the same thing, rendering a comment with the author.

How can we rewrite this to remove the duplicated code?

Although we can use a union type to indicate that the comment author can be a User or a Channel,

const renderComment: (author: User|Channel, comment: string) => React.ReactNode = (
  author,
  comment
) => { /* return comment */ };

In this scenario, it’s better to create one interface to represent the two types of users.

Let’s rewrite it using a more generic interface called Author.

interface Author { name: string; avatar: string; }

const renderComment: (author: Author, comment: string) => React.ReactNode = (
  author,
  comment
) => { /* return comment */};

const user = { name: "Ana", avatar: "ana.png", github: "analizapandac" };
const channel = { name: "Tedx Talks", avatar: "tedxtalks.png", channelId: "UCsT0YIqwnpJCM-mx7-gSA4Q" };

renderComment(user, 'hello there');
renderComment(channel, 'hi, thanks for watching');

Even though user and channel represents two different types of data, they satisfy the requirements to be considered an author which is a name and avatar so the renderComment function works for both objects.

The examples above are very basic but the point here is that we can use a generic interface to represent different objects which can work with different functions. This is one way to use interfaces to write more reusable code.

Closing Thoughts

Aside from learning about the what, why and how of interfaces, I hope that I have also shown you a glimpse of the power of interfaces and how you can use it to create amazing applications.

There’s a lot more to interfaces especially on how to use it together with the other TypeScript features such as classes and write even better code.

I’ll continue learning the more advanced TS concepts, creating new exciting projects using them and sharing them with you.

I hope you’ll do the same 😊

Project Github Repo

Here’s the source code of the app: https://github.com/analizapandac/tedflix

#typescript #react #javascript #angular

Learning TypeScript with React
17.55 GEEK