JavaScript is a dynamically typed programming language, meaning it doesn’t catch any type errors during build time. That’s where TypeScript comes into play.
TypeScript is a programming language that acts as a superset of JavaScript, which allows us to write JavaScript with some behaviors of a statically typed language. This comes in handy as we can catch many potential bugs before they get into production.
Why TypeScript?
TypeScript is especially useful for large applications built by large teams. Code written in TypeScript is much better documented than code written in vanilla JavaScript. By looking at the type definitions, we can figure out how a piece of code is supposed to work.
Another reason is that TypeScript makes refactoring much easier because most of the issues can be caught before running the application.
TypeScript also helps us utilize our editor’s IntelliSense, which shows us intelligent code completion, hover information, and signature information, which speeds up our productivity.
TypeScript setup
Our project already has TypeScript configured. The TypeScript configuration is defined in the tsconfig.json
file at the root of the project. It allows us to configure how strict we want it to be based on our needs:
{
"compilerOptions": {
"target": "es5",
"lib": ["dom", "dom.iterable", "esnext"],
"allowJs": true,
"skipLibCheck": true,
"strict": true,
"forceConsistentCasingInFileNames": true,
"noEmit": true,
"esModuleInterop": true,
"module": "esnext",
"moduleResolution": "Node",
"resolveJsonModule": true,
"isolatedModules": true,
"jsx": "preserve",
"incremental": true,
"baseUrl": ".",
"paths": {
"@/*": ["./src/*"]
}
},
"include": ["next-env.d.ts", "src"],
"exclude": ["node_modules"]
}
We will not dive too deeply into every configuration property since most of the properties have been auto-generated. However, there is one thing that was also provided:
"baseUrl": ".",
"paths": {
"@/*": ["./src/*"]
}
This will tell the TypeScript compiler that anything imported via @/*
will refer to the src
folder.
Previously, we had to perform messy imports, like so:
import { Component } from '../../../components/component'
Now, we can import components like so:
import { Component } from '@/components/component'
No matter how many nested levels we have, we can always import with absolute paths, and we will not be required to change our import statement should we decide to move the consumer file somewhere else.
Basic TypeScript usage
Let’s cover some TypeScript basics so that we are comfortable using it throughout this book.
Primitive types
let numberVar: number;
numberVar = 1 // OK
numberVar = "1" // Error
let stringVar: string;
stringVar = "Hi"; // OK
stringVar = false; // Error
let stringVar: string;
stringVar = "Hi"; // OK
stringVar = false; // Error
As we can see, we are only allowed to assign values with the corresponding type. Assigning to any other type except the any
type, which we will cover in a moment, will cause a TypeScript error.
Any
The any
type is the loosest type in TypeScript and using it will disable any type checking. We can use it when we want to bypass errors that would usually occur. However, we should only use it as a last resort and try to use other types first:
let anyVar: any;
anyVar = 1; // OK
anyVar = "Hello" // OK
anyVar = true; // OK
numberVar = anyVar; // OK
As we can see, variables with the any
type can accept and be assigned to a value of any other type, which makes it very flexible.
Unknown
Sometimes, we can’t know upfront which types we will have. This might happen with some dynamic data where we don’t know its type yet. Here, we can use the unknown
type:
let unknownVar: unknown;
unknownVar = 1; // OK
unknownVar = "123" // OK
let unknownVar2: unknown;
unknownVar = unknownVar2; // OK
anyVar = unknownVar2; // OK
numberVar = unknownVar2; // Error
stringVar = unknownVar2; // Error
booleanVar = unknownVar2; // Error
As we can see, we can assign values of any type to the variable with unknown
type
. However, we can only assign values with type unknown
to the variables with any
and unknown
types.
Arrays
There are two ways to define array types with TypeScript:
type numbers = number[]
type strings = Array<string>
Objects
Object shapes can be defined in two ways:
type Person = {
name: string;
age: number;
}
interface Person {
name: string;
age: number;
}
The first one is called type alias, while the second is called interface.
There are a few differences between type aliases and interfaces, but we won't get into them right now. For any object shape type we define, we can use type aliases.
Unions
The basic types we just mentioned are great, but sometimes, we want to allow a variable to be one of many types. Let’s look at the following example:
type Content = string | number;
let content: Content;
content = 1 // OK
content = "Hi"; // OK
content = false // Error
As we can see, the content
variable can now be either string
or number
.
We can also add literal types in the union, as shown in the following example:
type Color = "red" | "green" | "blue";
let color: Color;
color = "red" // OK
color = "yellow" // Error
Here, we are defining colors as strings, but we want to add more constraints so that we can only take one of those three colors. If we try to add anything else, TypeScript will warn us with an error.
Intersections
Intersection types allow us to combine the properties of two different objects into a single type. Consider this example:
type Foo = {
x: string;
y: number;
}
type Bar = {
z: boolean;
}
type FooBar = Foo & Bar;
The FooBar
type will now contain the x
, y
, and z
properties.
Generics
Generics is a mechanism of creating reusable types by parameterizing them. They can help us reduce code repetition. Consider the following type:
type Foo = {
x: number;
}
Let’s see what happens if we need the same structure but with x
as a string:
type Foo = {
x: string;
}
Here, we can see that there is some code duplication going on. We can simplify this by making it generic so that it accepts the type as T
. This would be assigned as the type of the x
property:
type Foo<T> = {
x: T;
}
let x: Foo<number>;
let y: Foo<string>;
Now, we have a nice way to reuse the structure by passing different types to the generic.
We can also use generics with functions:
function logger<T>(value: T) {
console.log(value)
}
logger<number>(1) // OK
logger<string>(1); // Error
To try out these snippets and see how different types behave, go to https://www.typescriptlang.org/play, copy the snippets, and play around with the types to see how they work.
TypeScript and React
Every TypeScript file that uses JSX must have the .
tsx
extension.
Typing React components is very straightforward:
type InfoProps = {
name: string;
age: number
};
const Info = (props: InfoProps) => {
return <div>{props.name}-{props.age}</div>;
};
These examples are pretty trivial. We will see more practical examples in the upcoming chapters when we start building the application. To learn more about TypeScript, it is recommended to check the TypeScript handbook at https://www.typescriptlang.org/docs, which covers all these topics in much more detail.