Introduction
TypeScript has grown to be one of the most popular and widely used programming language in web development. TypeScript is a superset of JavaScript. That simply means TypeScript is essentially JavaScript with some additional features. The goal of TypeScript is to write strongly typed JavaScript. Strong typing helps to scale web applications with minimal bugs. TypeScript code is converted to JavaScript by use of a compiler like TypeScript Compiler or Babel.
For a better understanding, we will develop a Student Application using React and TypeScript. This tutorial will give you everything you need to get started with TypeScript in your next project.
If you would prefer to follow this tutorial on YouTube it's available at the link below.
The final solution is available on GitHub
tndungu / React-TypeScript-StudentApp
React TypeScript Student App
React TypeScript Student App
A Student App using React TypeScript. Includes features like Type Inference, Type Annotation, Union Types, Interfaces, Enums and Generics.
Local Setup
- Clone the Repository using the following command: git clone https://github.com/tndungu/React-TypeScript-StudentApp.git
- Open the Repository using your favorite text editor. I use Visual Studio Code as a personal preference.
- Open terminal and run the following: npm install
- Run the project using npm start. This will open the project in http://localhost:3000
Video
There is a step by step guide on building the project on YouTube.
Prerequisites
This tutorial assumes you have some basic knowledge of React
Why use TypeScript?
There are many benefits of using typescript. The main ones are listed below:
- Strong typing ensures bugs are caught during development as opposed to being caught while the application is in Production. Also makes it easy to debug code.
- Documentation - It serves as documentation for JavaScript code making it easy to read and maintain.
- Saves development time.
- Generics in TypeScript provides a powerful type system that gives developers a lot of flexibility.
Student App in TypeScript
We will build an app using React that will cover the following aspects of TypeScript.
- Props
- Type inference vs Type annotation
- Union Types
- Organizing interfaces
- Enums
- Generics
App Development: Step by Step Guide
To start a new typescript app, use the following command
- yarn:
yarn create-react-app student-app --template typescript
- npm:
npx create-react-app student-app --template typescript
cd into student-app and yarn start
OR npm start
if using npm.
Props
We will start by passing a prop to the <App/>
component. It will be a string that will have the name of the app. Here we will see our first use case for TypeScript.
Modify the App.tsx
and index.tsx
files to look as below. Delete the App.test.tsx
file.
//Index.tsx
import React from 'react';
import ReactDOM from 'react-dom/client';
import './index.css';
import App from './App';
const root = ReactDOM.createRoot(
document.getElementById('root') as HTMLElement
);
root.render(
<React.StrictMode>
<App name="Student App" />
</React.StrictMode>
);
//App.tsx
export interface AppName {
name: string;
}
function App({ name }: AppName) {
return (
<div className="App">
<h1>{name}</h1>
</div>
);
}
export default App;
On your browser, you should be able to see Student App displayed. As we can see, AppName is an Interface that has a property called name.
An Interface is an abstract type that the compiler uses to know which property names a given object can have. It's used for type checking.
In the AppName
Interface, the property name is a string, that's why we are passing a string to the App
component. If you try to pass any other type like integer or boolean the App will give an error. It will not even compile. If you were not using TypeScript, the app will compile but give unexpected results later after deployment.
Type inference vs Type annotation
const [studentId,setStudentId] = useState(0)
setStudentId('test')
If you try the code above, it will not compile, typescript will give an error. This is because typescript has already inferred the type of studentId
to be an integer(number). If you try to assign a string to studentId
will therefore generate an error at compile time. This is referred to as type inference in TypeScript.
Similarly, the code below will not run in TypeScript.
const [student, setStudent] = useState(null)
setStudent({
name: 'Antony',
surname: 'Ndungu',
age: 15
})
This is because TypeScript infers the student object to be of type null and therefore we have to explicitly define the student object and in the useState()
hook we have to tell TypeScript user can be either null or student object. This is referred to as Type annotation. We will do that using the Student
interface. The final code of our App.tsx
will look as follows:
import { useState } from "react";
import './App.css'
export interface AppName {
name: string;
}
export interface Student {
name: string;
surname: string;
age?: number;
address?: {
houseNo: number;
street: string;
Town: string;
}
}
function App({ name }: AppName) {
const [student, setStudent] = useState<Student | null>(null)
const addStudent = () => {
setStudent({
name: 'Antony',
surname: 'Ndungu',
age: 20
})
}
return (
<div className="App">
<h1>{name}</h1>
<p><b>{student?.name} {student?.surname}</b></p>
<button onClick={addStudent}> Add Student</button>
</div>
);
}
export default App;
From the above code, the student can either be null
or Student
object. This is denoted by the code useState<Student | null>(null)
. This introduces another concept called Union Types.
Union Types
This is when you have an object that can be of having different types. For instance you might have const [student, setStudent] = useState<Student | null | boolean>(null)
. In this case Student | null | boolean
are Union Types.
Organizing Interfaces
There are 2 issues as far as our interfaces are concerned:
- We should not nest objects as we have done in the
Student
interface. Instead, we should have another interface for Address. - The Interfaces should be on their separate module for ease of maintenance and re-use.
We will create a new Interface for Address
. We will then create a new module for interfaces by creating an interfaces.ts
file inside the src
folder and moving the interfaces there. We will then import our interfaces in the App.tsx
file. The final App.tsx
and Interfaces.ts
files will look as follows:
//App.tsx
import { useState } from "react";
import './App.css'
import { Student, AppName } from './interfaces'
function App({ name }: AppName) {
const [student, setStudent] = useState<Student | null>(null)
const addStudent = () => {
setStudent({
name: 'Antony',
surname: 'Ndungu',
age: 20
})
}
return (
<div className="App">
<h1>{name}</h1>
<p><b>{student?.name} {student?.surname}</b></p>
<button onClick={addStudent}> Add Student</button>
</div>
);
}
export default App;
//interfaces.tsx
export interface AppName {
name: string;
}
export interface Address {
houseNo: number;
street: string;
Town: string;
}
export interface Student {
name: string;
surname: string;
age?: number;
address?: Address
}
Enums
An Enum is a type for holding constant values. In our example, the student level can either be "Undergraduate" or "Postgraduate".
export enum Level {
Undergraduate = "Undergraduate",
Postgraduate = "Postgraduate"
}
The above enum can be used to conditionally display the age of a student based on the student's level as shown below:
{
student?.level === Level.Undergraduate &&
<p><b>Age: {student.age}</b></p>
}
Generics
Generics are an important feature of TypeScript that is used for creating reusable components. The same component can be used to handle different data types as shown below.
Display both Students' and Courses' lists using the same component.
For our student App, I would like to display 2 lists: One for the students' list and another one for the courses' list. Without generics, I will end up creating 2 components that will be used to display the 2 lists. However, with Generics I will use only one component to display both lists. The DisplayData
component can be re-used to display any list of items even as our app grows bigger.
In src
folder, I have created DisplayData.tsx
component. The file looks as follows:
interface Item {
id: number;
}
interface DisplayDataItem<T> {
items: Array<T>
}
export const DisplayData = <T extends Item>({ items }: DisplayDataItem<T>) => {
return (
<>
<ul>
{items.map((item) => (
<li key={item.id}>{JSON.stringify(item)}</li>
))}
</ul>
</>
)
}
Interface Item
has a property id
which means any object that uses this component must have an id
property. Interface DisplayDataItem<T>
is an object that represents an Array<T>
of type T
which means it can be used by any object which consists of an array of items. DisplayData
is a function that accepts an array of items and displays the list.
The following is the final code for App.tsx
, App.css
and data.ts
files.
//App.tsx
import { useState } from "react";
import './App.css'
import { Student, AppName, Level } from './interfaces'
import { studentList, coursesList } from "./data";
import { DisplayData } from "./DisplayData";
function App({ name }: AppName) {
const [student, setStudent] = useState<Student | null>(null)
const addStudent = () => {
setStudent({
name: 'Antony',
surname: 'Ndungu',
age: 20,
level: "Undergraduate"
})
}
return (
<div className="App">
<h1>{name}</h1>
<p><b>{student?.name} {student?.surname}</b></p>
{student?.level === Level.Undergraduate &&
<p><b>Age: {student.age}</b></p>
}
<button onClick={addStudent}> Add Student</button>
<h3>List of Students</h3>
<div>
<DisplayData items={studentList} />
</div>
<h3>List of Courses</h3>
<div>
<DisplayData items={coursesList} />
</div>
</div>
);
}
export default App;
//data.ts
export const studentList = [
{ id: 1, name: 'Antony', surname: 'Ndungu', level: 'Undergraduate', age: 20 },
{ id: 2, name: 'Chanelle', surname: 'John', level: 'Postgraduate', age: 50 },
{ id: 3, name: 'Ian', surname: 'Smith', level: 'Undergraduate', age: 46 },
{ id: 4, name: 'Michael', surname: 'Starke', level: 'Postgraduate', age: 64 },
{ id: 5, name: 'Chris', surname: 'De Kock', level: 'Undergraduate', age: 19 },
]
export const coursesList = [
{ id: 1, code: 'A141', name: 'Algorithms Analysis', description: 'Analysis & Design' },
{ id: 1, code: 'BN445', name: 'Computer Architecture I', description: 'Computer Architecture' },
{ id: 1, code: 'P888', name: 'Operations Research', description: 'Maths - Operations Research' },
{ id: 1, code: 'Z9989', name: 'Discrete Maths', description: 'Discrete Mathematics' }
]
.App {
display: flex;
width: 100%;
align-items: center;
justify-content: center;
flex-direction: column;
}
li{
list-style-type: none;
}
button {
height: 30px;
width: 150px;
background-color: turquoise;
border-radius: 5px;
}
Generic Search Function
Finally, we will add a Generic search where the student list can be sorted based on either student name or Age on a button click.
Create a GenericSort.ts
file and ensure you have the following code. This code takes a list of array items and key for sorting then returns the sorted list. For example, if I would like to sort the student's list based on student name I will call the function GenericSort(studentList,"name")
This is a powerful use case for generics, I can use it if I want to sort the student records list based on different sorting columns. Implementing this without TypeScript would end up with many functions that are difficult to extend.
//GenericSort
export const GenericSort = <T,>(items: Array<T>, key: keyof T) => {
items.sort((a, b) => {
if (a[key] > b[key]) {
return 1;
}
if (a[key] < b[key]) {
return -1;
}
return 0;
})
return items
}
//App.tsx
import { useState } from "react";
import './App.css'
import { Student, AppName, Level } from './interfaces'
import { studentList, coursesList } from "./data";
import { DisplayData } from "./DisplayData";
import { GenericSort } from "./GenericSort";
function App({ name }: AppName) {
const [student, setStudent] = useState<Student | null>(null)
const [list, setList] = useState(studentList)
const addStudent = () => {
setStudent({
name: 'Antony',
surname: 'Ndungu',
age: 20,
level: "Undergraduate"
})
}
const sortData = () => {
GenericSort(studentList, "age")
setList([...studentList])
}
return (
<div className="App">
<h1>{name}</h1>
<p><b>{student?.name} {student?.surname}</b></p>
{student?.level === Level.Undergraduate &&
<p><b>Age: {student.age}</b></p>
}
<button onClick={addStudent}> Add Student</button>
<br />
<button onClick={sortData}>Sort Data</button>
<h3>List of Students</h3>
<div>
<DisplayData items={list} />
</div>
<h3>List of Courses</h3>
<div>
<DisplayData items={coursesList} />
</div>
</div>
);
}
export default App;
Conclusion
Awesome! at this point, you have gone through the core building blocks of building a React App using TypeScript. In the final part of our project, we went through an introduction to some advanced features of TypeScript, Generics.
Happy coding with TypeScript.
Feel free to comment below in case you need further assistance.
Top comments (4)
Great guide.
Thank you for sharing!
You're welcome.
Thanks for the tip, a cool feature I hadn't used before.