⚡️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.
I generated the project using Create React App with TypeScript
create-react-app my-app-name --typescript
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.
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:
[]
after the type (e.g. let names: string[]
)See here for a more comprehensive list of types.
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.
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:
const add = (a:number, b:number):number => {
return a + b;
}
// or
const add: (a: number, b: number) => number = (a, b) => {
return a + b;
};
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.
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.
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.
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.
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:
thumbnailURL
to imageURL
videoId
from string
to a numberApplying 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.
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 caseVideo
.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;
}
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?
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.
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.
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.
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 😊
Here’s the source code of the app: https://github.com/analizapandac/tedflix
#typescript #react #javascript #angular