The Ultimate TypeScript Guide: From Novice to Expert
The Ultimate TypeScript Guide: From Novice to Expert
Table of Contents
- Introduction
- Basic Types
- Advanced Types
- Functions
- Classes and Interfaces
- Generics
- Type Manipulation
- Modules and Namespaces
- Decorators
- Advanced Patterns
- TypeScript Compiler and Configuration
- TypeScript with React
- Testing in TypeScript
- Performance Optimization
- Real-World Project: Building a Type-Safe API Client
Introduction
TypeScript is a powerful superset of JavaScript that adds static typing and other features to enhance developer productivity and code quality. This guide will take you from the basics to advanced concepts, providing a comprehensive understanding of TypeScript.
Basic Types
TypeScript provides several basic types to help you describe the shape of your data:
// Boolean
let isDone: boolean = false;
// Number
let decimal: number = 6;
let hex: number = 0xf00d;
let binary: number = 0b1010;
let octal: number = 0o744;
// String
let color: string = "blue";
color = "red";
// Array
let list: number[] = [1, 2, 3];
let fruits: Array<string> = ["apple", "banana", "orange"];
// Tuple
let x: [string, number];
x = ["hello", 10]; // OK
// x = [10, "hello"]; // Error
// Enum
enum Color {
Red,
Green,
Blue,
}
let c: Color = Color.Green;
// Any
let notSure: any = 4;
notSure = "maybe a string instead";
notSure = false; // okay, definitely a boolean
// Void
function warnUser(): void {
console.log("This is my warning message");
}
// Null and Undefined
let u: undefined = undefined;
let n: null = null;
// Never
function error(message: string): never {
throw new Error(message);
}
// Object
let obj: object = { key: "value" };
Advanced Types
TypeScript offers advanced type features for more complex scenarios:
Union Types
function padLeft(value: string, padding: string | number) {
// ...
}
let indentedString = padLeft("Hello world", 4); // Works
Intersection Types
interface ErrorHandling {
success: boolean;
error?: { message: string };
}
interface ArtworksData {
artworks: { title: string }[];
}
type ArtworksResponse = ErrorHandling & ArtworksData;
const response: ArtworksResponse = {
success: true,
artworks: [{ title: "Mona Lisa" }],
};
Type Aliases
type Name = string;
type NameResolver = () => string;
type NameOrResolver = Name | NameResolver;
function getName(n: NameOrResolver): Name {
if (typeof n === "string") {
return n;
} else {
return n();
}
}
Literal Types
type Easing = "ease-in" | "ease-out" | "ease-in-out";
function animate(dx: number, dy: number, easing: Easing) {
// ...
}
animate(0, 0, "ease-in");
// animate(0, 0, "linear"); // Error: Argument of type '"linear"' is not assignable to parameter of type 'Easing'.
Index Types
function pluck<T, K extends keyof T>(o: T, propertyNames: K[]): T[K][] {
return propertyNames.map((n) => o[n]);
}
interface Car {
manufacturer: string;
model: string;
year: number;
}
let taxi: Car = {
manufacturer: "Toyota",
model: "Camry",
year: 2014,
};
let makeAndModel: string[] = pluck(taxi, ["manufacturer", "model"]);
Functions
TypeScript enhances JavaScript functions with type annotations and more:
Function Types
function add(x: number, y: number): number {
return x + y;
}
let myAdd: (x: number, y: number) => number = function (
x: number,
y: number
): number {
return x + y;
};
Optional and Default Parameters
function buildName(firstName: string, lastName?: string) {
if (lastName) return firstName + " " + lastName;
else return firstName;
}
function buildName2(firstName: string, lastName = "Smith") {
return firstName + " " + lastName;
}
Rest Parameters
function buildName(firstName: string, ...restOfName: string[]) {
return firstName + " " + restOfName.join(" ");
}
let employeeName = buildName("Joseph", "Samuel", "Lucas", "MacKinzie");
Function Overloads
function padding(all: number);
function padding(topAndBottom: number, leftAndRight: number);
function padding(top: number, right: number, bottom: number, left: number);
function padding(a: number, b?: number, c?: number, d?: number) {
if (b === undefined && c === undefined && d === undefined) {
b = c = d = a;
} else if (c === undefined && d === undefined) {
c = a;
d = b;
}
return {
top: a,
right: b,
bottom: c,
left: d,
};
}
Classes and Interfaces
TypeScript provides full support for classes and interfaces:
Classes
class Animal {
private name: string;
constructor(theName: string) {
this.name = theName;
}
move(distanceInMeters: number = 0) {
console.log(`${this.name} moved ${distanceInMeters}m.`);
}
}
class Snake extends Animal {
constructor(name: string) {
super(name);
}
move(distanceInMeters = 5) {
console.log("Slithering...");
super.move(distanceInMeters);
}
}
Interfaces
interface ClockInterface {
currentTime: Date;
setTime(d: Date): void;
}
class Clock implements ClockInterface {
currentTime: Date = new Date();
setTime(d: Date) {
this.currentTime = d;
}
constructor(h: number, m: number) {}
}
Generics
Generics allow you to create reusable components:
function identity<T>(arg: T): T {
return arg;
}
let output = identity<string>("myString");
class GenericNumber<T> {
zeroValue: T;
add: (x: T, y: T) => T;
}
let myGenericNumber = new GenericNumber<number>();
myGenericNumber.zeroValue = 0;
myGenericNumber.add = function (x, y) {
return x + y;
};
Generic Constraints
interface Lengthwise {
length: number;
}
function loggingIdentity<T extends Lengthwise>(arg: T): T {
console.log(arg.length); // Now we know it has a .length property, so no more error
return arg;
}
Type Manipulation
TypeScript provides powerful ways to manipulate and transform types:
Mapped Types
type Readonly<T> = {
readonly [P in keyof T]: T[P];
};
type Partial<T> = {
[P in keyof T]?: T[P];
};
interface Person {
name: string;
age: number;
}
type ReadonlyPerson = Readonly<Person>;
Conditional Types
type NonNullable<T> = T extends null | undefined ? never : T;
type ExtractType<T, U> = T extends U ? T : never;
type StringOrNumber = ExtractType<string | number | boolean, string | number>;
Infer Keyword
type ReturnType<T> = T extends (...args: any[]) => infer R ? R : any;
function f() {
return { x: 10, y: 3 };
}
type FReturnType = ReturnType<typeof f>; // { x: number, y: number }
Modules and Namespaces
TypeScript supports both ES modules and its own namespace system:
ES Modules
// math.ts
export function add(x: number, y: number): number {
return x + y;
}
// main.ts
import { add } from "./math";
console.log(add(1, 2));
Namespaces
namespace Validation {
export interface StringValidator {
isAcceptable(s: string): boolean;
}
const lettersRegexp = /^[A-Za-z]+$/;
const numberRegexp = /^[0-9]+$/;
export class LettersOnlyValidator implements StringValidator {
isAcceptable(s: string) {
return lettersRegexp.test(s);
}
}
export class ZipCodeValidator implements StringValidator {
isAcceptable(s: string) {
return s.length === 5 && numberRegexp.test(s);
}
}
}
let validators: { [s: string]: Validation.StringValidator } = {};
validators["ZIP code"] = new Validation.ZipCodeValidator();
validators["Letters only"] = new Validation.LettersOnlyValidator();
Decorators
Decorators provide a way to add both annotations and a meta-programming syntax for class declarations and members:
function sealed(constructor: Function) {
Object.seal(constructor);
Object.seal(constructor.prototype);
}
@sealed
class Greeter {
greeting: string;
constructor(message: string) {
this.greeting = message;
}
greet() {
return "Hello, " + this.greeting;
}
}
Advanced Patterns
Mixins
// Disposable Mixin
class Disposable {
isDisposed: boolean;
dispose() {
this.isDisposed = true;
}
}
// Activatable Mixin
class Activatable {
isActive: boolean;
activate() {
this.isActive = true;
}
deactivate() {
this.isActive = false;
}
}
class SmartObject implements Disposable, Activatable {
constructor() {
setInterval(
() => console.log(this.isActive + " : " + this.isDisposed),
500
);
}
interact() {
this.activate();
}
// Disposable
isDisposed: boolean = false;
dispose: () => void;
// Activatable
isActive: boolean = false;
activate: () => void;
deactivate: () => void;
}
applyMixins(SmartObject, [Disposable, Activatable]);
let smartObj = new SmartObject();
setTimeout(() => smartObj.interact(), 1000);
////////////////////////////////////////
// In your runtime library somewhere
////////////////////////////////////////
function applyMixins(derivedCtor: any, baseCtors: any[]) {
baseCtors.forEach((baseCtor) => {
Object.getOwnPropertyNames(baseCtor.prototype).forEach((name) => {
Object.defineProperty(
derivedCtor.prototype,
name,
Object.getOwnPropertyDescriptor(baseCtor.prototype, name)
);
});
});
}
Builder Pattern with Method Chaining
class RequestBuilder {
private data: object | null = null;
private method: "GET" | "POST" | "PUT" | "DELETE" | "PATCH" = "GET";
private url: string = "";
setMethod(method: "GET" | "POST" | "PUT" | "DELETE" | "PATCH"): this {
this.method = method;
return this;
}
setData(data: object): this {
this.data = data;
return this;
}
setURL(url: string): this {
this.url = url;
return this;
}
send(): Promise<Response> {
return fetch(this.url, {
method: this.method,
body: this.data ? JSON.stringify(this.data) : null,
headers: {
"Content-Type": "application/json",
},
});
}
}
// Usage
new RequestBuilder()
.setMethod("POST")
.setURL("https://api.example.com/data")
.setData({ name: "John Doe" })
.send()
.then((response) => response.json())
.then((data) => console.log(data));
TypeScript Compiler and Configuration
Understanding the TypeScript compiler (tsc) and its configuration options is crucial:
tsconfig.json
{
"compilerOptions": {
"target": "es5",
"module": "commonjs",
"lib": ["dom", "es2015"],
"strict": true,
"esModuleInterop": true,
"outDir": "./dist",
"rootDir": "./src",
"declaration": true,
"sourceMap": true
},
"include": ["src/**/*"],
"exclude": ["node_modules", "**/*.spec.ts"]
}
Compiler API
import * as ts from "typescript";
function compile(fileNames: string[], options: ts.CompilerOptions): void {
let program = ts.createProgram(fileNames, options);
let emitResult = program.emit();
let allDiagnostics = ts
.getPreEmitDiagnostics(program)
.concat(emitResult.diagnostics);
allDiagnostics.forEach((diagnostic) => {
if (diagnostic.file) {
let { line, character } = diagnostic.file.getLineAndCharacterOfPosition(
diagnostic.start!
);
let message = ts.flattenDiagnosticMessageText(
diagnostic.messageText,
"\n"
);
console.log(
`${diagnostic.file.fileName} (${line + 1},${character + 1}): ${message}`
);
} else {
console.log(
ts.flattenDiagnosticMessageText(diagnostic.messageText, "\n")
);
}
});
let exitCode = emitResult.emitSkipped ? 1 : 0;
console.log(`Process exiting with code '${exitCode}'.`);
process.exit(exitCode);
}
compile(process.argv.slice(2), {
noEmitOnError: true,
noImplicitAny: true,
target: ts.ScriptTarget.ES5,
module: ts.ModuleKind.CommonJS,
});
TypeScript with React
TypeScript integrates seamlessly with React, providing type safety for props, state, and more:
import React, { useState, useEffect } from "react";
interface User {
id: number;
name: string;
email: string;
}
interface UserListProps {
title: string;
}
const UserList: React.FC<UserListProps> = ({ title }) => {
const [users, setUsers] = useState<User[]>([]);
const [loading, setLoading] = useState<boolean>(true);
useEffect(() => {
fetchUsers();
}, []);
const fetchUsers = async () => {
try {
const response = await fetch("https://api.example.com/users");
const data: User[] = await response.json();
setUsers(data);
setLoading(false);
} catch (error) {
console.error("Error fetching users:", error);
setLoading(false);
}
};
return (
<div>
<h1>{title}</h1>
{loading ? (
<p>Loading...</p>
) : (
<ul>
{users.map((user) => (
<li key={user.id}>
{user.name} ({user.email})
</li>
))}
</ul>
)}
</div>
);
};
export default UserList;
Testing in TypeScript
Testing TypeScript code involves using testing frameworks like Jest along with TypeScript-specific tools:
// math.ts
export function add(a: number, b: number): number {
return a + b;
}
// math.test.ts
import { add } from "./math";
describe("add function", () => {
it("should add two numbers correctly", () => {
expect(add(1, 2)).toBe(3);
expect(add(-1, 1)).toBe(0);
expect(add(5, 5)).toBe(10);
});
it("should return NaN for invalid inputs", () => {
expect(add(1, NaN)).toBeNaN();
expect(add(NaN, 1)).toBeNaN();
});
});
Performance Optimization
TypeScript can help with performance optimization through proper typing and compiler optimizations:
// Using const assertions for better performance
const config = {
endpoint: "https://api.example.com",
apiKey: "your-api-key",
} as const;
// Type-based optimizations
type Vec2D = [number, number];
function addVectors(v1: Vec2D, v2: Vec2D): Vec2D {
return [v1[0] + v2[0], v1[1] + v2[1]];
}
// Performance-critical loop
function sumArray(arr: number[]): number {
let sum = 0;
for (let i = 0; i < arr.length; i++) {
sum += arr[i];
}
return sum;
}
// Using generics for flexible, type-safe functions
function quickSort<T>(arr: T[], compare: (a: T, b: T) => number): T[] {
if (arr.length <= 1) return arr;
const pivot = arr[Math.floor(arr.length / 2)];
const left = arr.filter((x) => compare(x, pivot) < 0);
const middle = arr.filter((x) => compare(x, pivot) === 0);
const right = arr.filter((x) => compare(x, pivot) > 0);
return [...quickSort(left, compare), ...middle, ...quickSort(right, compare)];
}
Real-World Project: Building a Type-Safe API Client
Let's build a type-safe API client using TypeScript, demonstrating many of the concepts we've covered:
// api-types.ts
export interface User {
id: number;
name: string;
email: string;
}
export interface Post {
id: number;
title: string;
body: string;
userId: number;
}
export interface Comment {
id: number;
postId: number;
name: string;
email: string;
body: string;
}
// api-client.ts
import axios, { AxiosInstance, AxiosResponse } from "axios";
import { User, Post, Comment } from "./api-types";
class APIClient {
private client: AxiosInstance;
constructor(baseURL: string) {
this.client = axios.create({
baseURL,
headers: {
"Content-Type": "application/json",
},
});
}
private async get<T>(url: string): Promise<T> {
const response: AxiosResponse<T> = await this.client.get(url);
return response.data;
}
private async post<T>(url: string, data: any): Promise<T> {
const response: AxiosResponse<T> = await this.client.post(url, data);
return response.data;
}
async getUsers(): Promise<User[]> {
return this.get<User[]>("/users");
}
async getUser(id: number): Promise<User> {
return this.get<User>(`/users/${id}`);
}
async getPosts(): Promise<Post[]> {
return this.get<Post[]>("/posts");
}
async getPost(id: number): Promise<Post> {
return this.get<Post>(`/posts/${id}`);
}
async getComments(postId: number): Promise<Comment[]> {
return this.get<Comment[]>(`/posts/${postId}/comments`);
}
async createPost(post: Omit<Post, "id">): Promise<Post> {
return this.post<Post>("/posts", post);
}
}
// Usage
const apiClient = new APIClient("https://jsonplaceholder.typicode.com");
async function fetchAndDisplayUserPosts(userId: number) {
try {
const user = await apiClient.getUser(userId);
console.log(`Fetching posts for user: ${user.name}`);
const posts = await apiClient.getPosts();
const userPosts = posts.filter((post) => post.userId === userId);
for (const post of userPosts) {
console.log(`Post: ${post.title}`);
const comments = await apiClient.getComments(post.id);
console.log(`Comments: ${comments.length}`);
}
} catch (error) {
console.error("Error fetching data:", error);
}
}
fetchAndDisplayUserPosts(1);
This project demonstrates:
- Type definitions for API responses
- A generic API client with type-safe methods
- Error handling
- Async/await usage
- Filtering and mapping of typed arrays
Conclusion
This comprehensive guide covers the breadth and depth of TypeScript, from basic concepts to advanced patterns and real-world applications. By mastering these concepts, you'll be well-equipped to build robust, scalable, and maintainable applications using TypeScript.