The Complete TypeScript Handbook
I recently dove deep into TypeScript, and honestly, it's been a game-changer for my development workflow. After spending months exploring everything from basic types to advanced patterns, I wanted to share what I've learned and the key insights that made the biggest difference. Here’s an improved version with better flow, clarity, and tone:
Reader Note - ⏰ Quick Heads-Up
This is a complete, hands-on guide—set aside 30–45 minutes of focused reading time. It’s packed with practical insights and real-world examples to help you truly master TypeScript.
Don’t be put off by the length—a big chunk is made up of simple, easy-to-follow code snippets.
Grab a coffee ☕️ and let’s dive in.
Why TypeScript Matters: The Great Type Divide
Before we dive into TypeScript specifics, let's understand the fundamental difference between programming languages and why TypeScript exists.
Strongly Typed vs Loosely Typed Languages
Programming languages fall into two main categories when it comes to type handling:
Aspect | Strongly Typed | Loosely Typed |
---|---|---|
Examples | Java, C++, C#, Rust, TypeScript | JavaScript, Python, PHP, Perl |
Type Checking | Enforced at compile-time | Determined at runtime |
Error Detection | Catch errors before code runs | Errors surface during execution |
Development Speed | Slower initially, faster long-term | Faster to write initially |
Tooling Support | Excellent IDE support & autocomplete | Limited compared to strongly typed |
Refactoring | Safer and easier | More error-prone |
Flexibility | Less flexible, more predictable | Highly flexible but error-prone |
Runtime Errors | Fewer type-related runtime errors | Can lead to unexpected runtime errors |
Code Example Comparison
Strongly Typed (C++):
#include <iostream>
int main() {
int number = 10;
number = "text"; // ❌ Compilation Error!
return 0;
}
Loosely Typed (JavaScript):
function main() {
let number = 10;
number = "text"; // ✅ Works fine (but may cause issues later)
return number;
}
How TypeScript Works
Here's the crucial thing to understand: TypeScript never runs in your browser. The workflow looks like this:
TypeScript Code (.ts) → TypeScript Compiler (tsc) → JavaScript Code (.js) → Browser/Node.js
The TypeScript compiler does two important things:
- Type checking - Catches errors before runtime
- Transpilation - Converts TS code to JS code
Setting Up Your First TypeScript Project
Let's get our hands dirty with a practical setup. I'll walk you through creating a TypeScript Node.js application from scratch.
Step 1: Install TypeScript Globally
npm install -g typescript
Step 2: Initialize Your Project
mkdir my-typescript-app
cd my-typescript-app
npm init -y
npx tsc --init
This creates two important files:
package.json
- Your Node.js project configurationtsconfig.json
- TypeScript compiler configuration
Step 3: Write Your First TypeScript Code
Create a file called app.ts
:
const message: string = "Hello, TypeScript!";
const count: number = 42;
const isActive: boolean = true;
console.log(`${message} Count: ${count}, Active: ${isActive}`);
Step 4: Compile and Run
# Compile TypeScript to JavaScript
tsc -b
# Run the generated JavaScript
node app.js
Step 5: See TypeScript in Action
Now let's see TypeScript catch an error. Modify your app.ts
:
let count: number = 42;
count = "Vishal Rajput"; // ❌ TypeScript will catch this error!
console.log(count);
Try compiling again:
tsc -b
You'll see an error message, and no JavaScript file will be generated. This is TypeScript protecting you from runtime errors!
Mastering Basic Types
TypeScript provides several fundamental types that form the building blocks of your applications.
Primitive Types
// Numbers - integers and floats
let age: number = 25;
let price: number = 99.99;
// Strings - text data
let firstName: string = "Vishal";
let lastName: string = "Rajput";
// Booleans - true/false values
let isStudent: boolean = true;
let hasJob: boolean = false;
// Null and Undefined
let emptyValue: null = null;
let notAssigned: undefined = undefined;
Function Types and Parameters
One of TypeScript's most powerful features is adding types to functions. Let me show you some practical examples:
Basic Function with Typed Parameters
function greetUser(firstName: string): void {
console.log(`Hello, ${firstName}! Welcome to our platform.`);
}
// Usage
greetUser("Vishal"); // ✅ Works
greetUser(123); // ❌ Error: Argument of type 'number' is not assignable to parameter of type 'string'
Functions with Return Types
function calculateSum(a: number, b: number): number {
return a + b;
}
function checkAge(age: number): boolean {
return age >= 18;
}
// Usage examples
const total = calculateSum(10, 15); // total is inferred as number
const isAdult = checkAge(21); // isAdult is inferred as boolean
console.log(`Total: ${total}, Is Adult: ${isAdult}`);
Higher-Order Functions
TypeScript really shines when working with functions that take other functions as parameters:
function executeAfterDelay(callback: () => void): void {
setTimeout(callback, 1000);
}
// Usage
executeAfterDelay(() => {
console.log("This runs after 1 second!");
});
Configuring TypeScript: The tsconfig.json File
The tsconfig.json
file is your control center for TypeScript compilation. Let me walk you through the most important options:
Essential Configuration Options
{
"compilerOptions": {
"target": "ES2020", // JavaScript version to compile to
"rootDir": "src", // Where your TypeScript files live
"outDir": "dist", // Where compiled JavaScript goes
"noImplicitAny": true, // Catch implicit 'any' types
"removeComments": true, // Clean up output files
"strict": true // Enable all strict type checking
}
}
Target Option Deep Dive
The target
option determines which JavaScript version your TypeScript compiles to. Here's a practical example:
TypeScript code:
const greetUser = (name: string) => `Hello, ${name}!`;
Compiled to ES5:
"use strict";
var greetUser = function (name) {
return "Hello, ".concat(name, "!");
};
Compiled to ES2020:
"use strict";
const greetUser = (name) => `Hello, ${name}!`;
Project Structure Best Practices
I recommend organizing your project like this:
my-typescript-app/
├── src/ # TypeScript source files
├── dist/ # Compiled JavaScript files
├── tsconfig.json # TypeScript configuration
└── package.json # Node.js configuration
Interfaces: Defining Object Shapes
Interfaces are one of TypeScript's most powerful features. They let you define contracts for object structures, making your code more predictable and maintainable.
Basic Interface Usage
Let's say you're building a user management system:
interface User {
firstName: string;
lastName: string;
email: string;
age: number;
isActive: boolean;
}
// Now you can use this interface to type objects
const user: User = {
firstName: "Vishal",
lastName: "Rajput",
email: "vishal.rajput@email.com",
age: 28,
isActive: true,
};
Practical Interface Examples
User Age Verification System
interface User {
firstName: string;
lastName: string;
email: string;
age: number;
}
function isEligibleToVote(user: User): boolean {
return user.age >= 18;
}
function getUserFullName(user: User): string {
return `${user.firstName} ${user.lastName}`;
}
// Usage
const vishal: User = {
firstName: "Vishal",
lastName: "Rajput",
email: "vishal@example.com",
age: 25,
};
console.log(`${getUserFullName(vishal)} can vote: ${isEligibleToVote(vishal)}`);
Todo Application Interface
interface Todo {
id: number;
title: string;
description: string;
completed: boolean;
createdAt: Date;
}
interface TodoProps {
todo: Todo;
onToggle: (id: number) => void;
onDelete: (id: number) => void;
}
// React component using the interface
function TodoItem({ todo, onToggle, onDelete }: TodoProps) {
return (
<div className={`todo-item ${todo.completed ? "completed" : ""}`}>
<h3>{todo.title}</h3>
<p>{todo.description}</p>
<button onClick={() => onToggle(todo.id)}>
{todo.completed ? "Mark Incomplete" : "Mark Complete"}
</button>
<button onClick={() => onDelete(todo.id)}>Delete</button>
</div>
);
}
Implementing Interfaces with Classes
Interfaces can also define contracts that classes must follow:
interface Employee {
name: string;
employeeId: number;
department: string;
calculateSalary(): number;
getDetails(): string;
}
class Developer implements Employee {
name: string;
employeeId: number;
department: string;
private hourlyRate: number;
private hoursWorked: number;
constructor(name: string, id: number, hourlyRate: number) {
this.name = name;
this.employeeId = id;
this.department = "Engineering";
this.hourlyRate = hourlyRate;
this.hoursWorked = 40; // Default full-time hours
}
calculateSalary(): number {
return this.hourlyRate * this.hoursWorked;
}
getDetails(): string {
return `${this.name} (ID: ${this.employeeId}) - ${this.department}`;
}
}
class Manager implements Employee {
name: string;
employeeId: number;
department: string;
private baseSalary: number;
private bonus: number;
constructor(
name: string,
id: number,
department: string,
baseSalary: number
) {
this.name = name;
this.employeeId = id;
this.department = department;
this.baseSalary = baseSalary;
this.bonus = 0;
}
calculateSalary(): number {
return this.baseSalary + this.bonus;
}
getDetails(): string {
return `${this.name} (ID: ${this.employeeId}) - Manager, ${this.department}`;
}
}
Types: Beyond Basic Interfaces
While interfaces are great for object shapes, TypeScript's type
keyword offers more flexibility and power.
Basic Type Definitions
type User = {
firstName: string;
lastName: string;
age: number;
email: string;
};
type UserId = string | number; // Union type
Union Types: Handling Multiple Possibilities
Union types are incredibly useful when dealing with values that could be one of several types:
type ID = string | number;
function printUserID(id: ID): void {
console.log(`User ID: ${id}`);
}
// Both of these work
printUserID(12345); // number
printUserID("USER_12345"); // string
Real-World Union Type Example
type APIResponse =
| {
success: true;
data: any;
}
| {
success: false;
error: string;
};
function handleAPIResponse(response: APIResponse): void {
if (response.success) {
console.log("Data received:", response.data);
} else {
console.error("API Error:", response.error);
}
}
Intersection Types: Combining Multiple Types
Intersection types let you combine multiple types into one:
type Employee = {
name: string;
employeeId: number;
startDate: Date;
};
type Manager = {
department: string;
teamSize: number;
budget: number;
};
type TeamLead = Employee & Manager;
const vishalTeamLead: TeamLead = {
name: "Vishal Rajput",
employeeId: 1001,
startDate: new Date("2023-01-15"),
department: "Engineering",
teamSize: 5,
budget: 100000,
};
When to Use Types vs Interfaces
Use Interfaces when:
- Defining object shapes
- Creating contracts for classes
- You need declaration merging
- Working with object-oriented patterns
Use Types when:
- Creating union types
- Creating intersection types
- Working with primitive types
- Creating utility types
Working with Arrays in TypeScript
Arrays in TypeScript are straightforward but powerful. Here's how to work with them effectively:
Basic Array Syntax
// Array of numbers
const numbers: number[] = [1, 2, 3, 4, 5];
// Array of strings
const names: string[] = ["Vishal", "Rajput", "Developer"];
// Alternative syntax (both are equivalent)
const ages: Array<number> = [25, 30, 35];
Practical Array Examples
Finding Maximum Value
function findMaximum(numbers: number[]): number {
if (numbers.length === 0) {
throw new Error("Array cannot be empty");
}
return Math.max(...numbers);
}
// Usage
const scores = [85, 92, 78, 96, 88];
console.log(`Highest score: ${findMaximum(scores)}`);
Filtering Users by Age
interface User {
firstName: string;
lastName: string;
age: number;
email: string;
}
function filterAdultUsers(users: User[]): User[] {
return users.filter((user) => user.age >= 18);
}
// Usage
const allUsers: User[] = [
{
firstName: "Vishal",
lastName: "Rajput",
age: 25,
email: "vishal@example.com",
},
{ firstName: "John", lastName: "Doe", age: 17, email: "john@example.com" },
{ firstName: "Jane", lastName: "Smith", age: 22, email: "jane@example.com" },
];
const adultUsers = filterAdultUsers(allUsers);
console.log(`Found ${adultUsers.length} adult users`);
Enums: Creating Named Constants
Enums help you create a set of named constants, making your code more readable and maintainable.
Basic Enum Usage
enum Direction {
Up,
Down,
Left,
Right,
}
function movePlayer(direction: Direction): void {
switch (direction) {
case Direction.Up:
console.log("Moving up!");
break;
case Direction.Down:
console.log("Moving down!");
break;
case Direction.Left:
console.log("Moving left!");
break;
case Direction.Right:
console.log("Moving right!");
break;
}
}
// Usage
movePlayer(Direction.Up);
Custom Enum Values
enum HTTPStatus {
OK = 200,
BadRequest = 400,
Unauthorized = 401,
NotFound = 404,
InternalServerError = 500,
}
enum LogLevel {
DEBUG = "DEBUG",
INFO = "INFO",
WARN = "WARN",
ERROR = "ERROR",
}
function logMessage(level: LogLevel, message: string): void {
console.log(`[${level}] ${new Date().toISOString()}: ${message}`);
}
// Usage
logMessage(LogLevel.INFO, "Application started successfully");
logMessage(LogLevel.ERROR, "Database connection failed");
Real-World Enum Example: API Response Handler
enum APIEndpoints {
USERS = "/api/users",
POSTS = "/api/posts",
COMMENTS = "/api/comments",
AUTH = "/api/auth",
}
enum RequestMethod {
GET = "GET",
POST = "POST",
PUT = "PUT",
DELETE = "DELETE",
}
interface APIRequest {
endpoint: APIEndpoints;
method: RequestMethod;
data?: any;
}
function makeAPICall(request: APIRequest): void {
console.log(`Making ${request.method} request to ${request.endpoint}`);
if (request.data) {
console.log("Request data:", request.data);
}
}
// Usage
makeAPICall({
endpoint: APIEndpoints.USERS,
method: RequestMethod.POST,
data: { name: "Vishal", email: "vishal@example.com" },
});
Generics: Writing Reusable Code
Generics are one of TypeScript's most powerful features. They allow you to write flexible, reusable code while maintaining type safety.
The Problem Generics Solve
Imagine you need a function that returns the first element of an array. Without generics, you might write:
// Bad approach - separate functions for each type
function getFirstString(arr: string[]): string {
return arr[0];
}
function getFirstNumber(arr: number[]): number {
return arr[0];
}
// Or worse - using 'any' (loses type safety)
function getFirstElement(arr: any[]): any {
return arr[0];
}
Generic Solution
function getFirstElement<T>(arr: T[]): T {
return arr[0];
}
// TypeScript infers the correct type automatically
const firstString = getFirstElement(["Vishal", "Rajput", "Developer"]); // string
const firstNumber = getFirstElement([1, 2, 3, 4, 5]); // number
const firstBoolean = getFirstElement([true, false]); // boolean
// You can also be explicit about the type
const explicitString = getFirstElement<string>(["Hello", "World"]);
Practical Generic Examples
Generic API Response Handler
interface APIResponse<T> {
data: T;
success: boolean;
message: string;
}
interface User {
id: number;
name: string;
email: string;
}
interface Post {
id: number;
title: string;
content: string;
authorId: number;
}
function handleAPIResponse<T>(response: APIResponse<T>): T | null {
if (response.success) {
console.log(response.message);
return response.data;
} else {
console.error("API Error:", response.message);
return null;
}
}
// Usage with different data types
const userResponse: APIResponse<User> = {
data: { id: 1, name: "Vishal Rajput", email: "vishal@example.com" },
success: true,
message: "User fetched successfully",
};
const postResponse: APIResponse<Post[]> = {
data: [
{
id: 1,
title: "TypeScript Guide",
content: "Learning TS...",
authorId: 1,
},
],
success: true,
message: "Posts fetched successfully",
};
const user = handleAPIResponse(userResponse); // User | null
const posts = handleAPIResponse(postResponse); // Post[] | null
Generic Storage Class
class Storage<T> {
private items: T[] = [];
add(item: T): void {
this.items.push(item);
}
get(index: number): T | undefined {
return this.items[index];
}
getAll(): T[] {
return [...this.items];
}
remove(index: number): T | undefined {
return this.items.splice(index, 1)[0];
}
size(): number {
return this.items.length;
}
}
// Usage with different types
const stringStorage = new Storage<string>();
stringStorage.add("Vishal");
stringStorage.add("Rajput");
const numberStorage = new Storage<number>();
numberStorage.add(42);
numberStorage.add(100);
const userStorage = new Storage<User>();
userStorage.add({ id: 1, name: "Vishal", email: "vishal@example.com" });
Modules: Organizing Your Code
As your TypeScript applications grow, organizing code into modules becomes essential. TypeScript follows the ES6 module system.
Named Exports and Imports
math.ts
export function add(x: number, y: number): number {
return x + y;
}
export function subtract(x: number, y: number): number {
return x - y;
}
export function multiply(x: number, y: number): number {
return x * y;
}
export const PI = 3.14159;
calculator.ts
import { add, subtract, multiply, PI } from "./math";
export class Calculator {
add(x: number, y: number): number {
return add(x, y);
}
subtract(x: number, y: number): number {
return subtract(x, y);
}
multiply(x: number, y: number): number {
return multiply(x, y);
}
calculateCircleArea(radius: number): number {
return PI * radius * radius;
}
}
Default Exports
user.ts
interface User {
id: number;
name: string;
email: string;
}
export default class UserManager {
private users: User[] = [];
addUser(user: User): void {
this.users.push(user);
}
getUserById(id: number): User | undefined {
return this.users.find((user) => user.id === id);
}
getAllUsers(): User[] {
return [...this.users];
}
}
app.ts
import UserManager from "./user";
import { Calculator } from "./calculator";
const userManager = new UserManager();
const calculator = new Calculator();
userManager.addUser({
id: 1,
name: "Vishal Rajput",
email: "vishal@example.com",
});
console.log("Users:", userManager.getAllUsers());
console.log("10 + 5 =", calculator.add(10, 5));
Advanced TypeScript Patterns
Utility Types
TypeScript provides several built-in utility types that make working with existing types easier:
interface User {
id: number;
name: string;
email: string;
age: number;
isActive: boolean;
}
// Partial - makes all properties optional
type PartialUser = Partial<User>;
function updateUser(id: number, updates: PartialUser): void {
// Implementation here
}
// Pick - select specific properties
type UserSummary = Pick<User, "id" | "name" | "email">;
// Omit - exclude specific properties
type CreateUserData = Omit<User, "id">;
// Required - makes all properties required
type RequiredUser = Required<PartialUser>;
Conditional Types
type NonNullable<T> = T extends null | undefined ? never : T;
type UserEmail = NonNullable<string | null>; // string
Best Practices and Pro Tips
1. Use Strict Mode
Always enable strict mode in your tsconfig.json
:
{
"compilerOptions": {
"strict": true,
"noImplicitAny": true,
"strictNullChecks": true,
"strictFunctionTypes": true
}
}
2. Prefer Types for Complex Unions
// Good
type Status = "loading" | "success" | "error";
// Less ideal for this use case
interface StatusInterface {
status: "loading" | "success" | "error";
}
3. Use Readonly for Immutable Data
interface ReadonlyUser {
readonly id: number;
readonly email: string;
name: string; // This can still be modified
}
4. Leverage Type Guards
function isString(value: unknown): value is string {
return typeof value === "string";
}
function processValue(value: unknown): void {
if (isString(value)) {
// TypeScript knows value is string here
console.log(value.toUpperCase());
}
}
Common Pitfalls to Avoid
1. Overusing any
// Bad
function processData(data: any): any {
return data.something.else;
}
// Good
interface DataStructure {
something: {
else: string;
};
}
function processData(data: DataStructure): string {
return data.something.else;
}
2. Not Using Union Types When Appropriate
// Bad
function getId(): any {
return Math.random() > 0.5 ? 123 : "user_123";
}
// Good
function getId(): number | string {
return Math.random() > 0.5 ? 123 : "user_123";
}
3. Ignoring Null/Undefined Checks
// Bad
function getUserName(user: User): string {
return user.name.toUpperCase(); // What if name is undefined?
}
// Good
function getUserName(user: User): string {
return user.name?.toUpperCase() ?? "Unknown User";
}
Quick Reference Cheat Sheet
Basic Types
let num: number = 42;
let str: string = "Hello";
let bool: boolean = true;
let arr: number[] = [1, 2, 3];
let tuple: [string, number] = ["age", 25];
Function Types
function greet(name: string): string {
return `Hello ${name}`;
}
const add = (a: number, b: number): number => a + b;
type Handler = (event: string) => void;
Object Types
interface User {
name: string;
age: number;
}
type Point = { x: number; y: number };
Advanced Types
type ID = string | number; // Union
type Employee = Person & Worker; // Intersection
type Status = "idle" | "loading"; // Literal types
Conclusion
TypeScript has fundamentally changed how I approach JavaScript development. The static typing, excellent tooling, and enhanced developer experience make it an invaluable tool for building robust applications.
Remember, the goal isn't to use every TypeScript feature in every project. Start with the basics - proper typing for functions and objects - and gradually incorporate more advanced features as you become comfortable.
Frequently Asked Questions
1. When should I use TypeScript over JavaScript?
Use TypeScript when:
- Building medium to large applications
- Working in a team environment
- You want better IDE support and refactoring capabilities
- Type safety is important for your project
- You're building libraries or APIs that others will consume
Stick with JavaScript for:
- Quick prototypes or scripts
- Very small projects
- When the team is not familiar with TypeScript and learning time is limited
2. What's the difference between interface
and type
?
Use interface
when:
- Defining object shapes
- You need declaration merging
- Creating contracts for classes
- Working with object-oriented patterns
Use type
when:
- Creating union types (
string | number
) - Creating intersection types (
A & B
) - Working with primitive types
- Creating utility types
3. How do I handle null
and undefined
in TypeScript?
Enable strictNullChecks
in your tsconfig.json and use:
- Optional chaining:
user?.name?.toUpperCase()
- Nullish coalescing:
user.name ?? "Default Name"
- Type guards:
if (user.name !== null) { ... }
- Non-null assertion (use sparingly):
user.name!.toUpperCase()
4. What are generics and when should I use them?
Generics allow you to create reusable components that work with multiple types while maintaining type safety. Use them when:
- Writing utility functions that work with different types
- Creating data structures (arrays, maps, etc.)
- Building APIs that need to handle various data types
- You find yourself duplicating code for different types
5. How do I migrate an existing JavaScript project to TypeScript?
- Install TypeScript:
npm install -D typescript @types/node
- Create
tsconfig.json
:npx tsc --init
- Rename
.js
files to.ts
gradually - Start with
"noImplicitAny": false
and enable it later - Add types incrementally, starting with function parameters and return types
- Install type definitions for external libraries:
npm install -D @types/library-name
6. What are the most common TypeScript compilation errors?
- Type 'X' is not assignable to type 'Y': Usually a type mismatch. Check your variable types.
- Object is possibly 'null' or 'undefined': Enable null checks and handle null cases.
- Property 'X' does not exist on type 'Y': The property doesn't exist on the object type. Check your interface/type definitions.
- Cannot find module 'X': Missing type definitions. Install
@types/X
or create your own.
7. How do I type React components in TypeScript?
import React from "react";
interface Props {
name: string;
age?: number;
onSave: (data: string) => void;
}
const UserCard: React.FC<Props> = ({ name, age, onSave }) => {
return (
<div>
<h2>{name}</h2>
{age && <p>Age: {age}</p>}
<button onClick={() => onSave(name)}>Save</button>
</div>
);
};
8. Should I use classes or functions with TypeScript?
Both have their place:
- Functions: Better for functional programming, easier to test, work well with React hooks
- Classes: Good for object-oriented patterns, when you need inheritance, or working with existing OOP codebases
9. How do I properly type async functions and Promises?
TypeScript automatically infers Promise types, but you can be explicit:
// Function returns Promise<string>
async function fetchUserName(id: number): Promise<string> {
const response = await fetch(`/api/users/${id}`);
const user = await response.json();
return user.name;
}
// Using with try-catch
async function safeApiCall(): Promise<string | null> {
try {
const result = await fetchUserName(123);
return result;
} catch (error) {
console.error("API call failed:", error);
return null;
}
}
// Typing Promise.all
const results = await Promise.all([
fetchUserName(1), // Promise<string>
fetchUserName(2), // Promise<string>
fetchUserName(3), // Promise<string>
]); // results is string[]
10. What's the best way to handle complex nested object types?
Use a combination of interfaces, utility types, and type composition:
// Base interfaces
interface Address {
street: string;
city: string;
zipCode: string;
country: string;
}
interface User {
id: number;
name: string;
email: string;
address: Address;
preferences: {
notifications: boolean;
theme: "light" | "dark";
language: string;
};
}
// Utility types for partial updates
type UserUpdate = Partial<Pick<User, "name" | "email">>;
type AddressUpdate = Partial<Address>;
// Nested partial for complex updates
type UserProfileUpdate = {
user?: UserUpdate;
address?: AddressUpdate;
preferences?: Partial<User["preferences"]>;
};
// Using mapped types for form data
type UserFormData = {
[K in keyof User]: User[K] extends object
? string // Convert nested objects to strings for form handling
: User[K];
};
"Take a break if you made it through the blog in one go — well done! Keep practicing TypeScript consistently to sharpen your skills."