I've extended the TypeScript tutorial with all the requested topics:
9. Generics
- Basic generic functions, interfaces, and classes
- Type inference and constraints
- Practical examples showing how generics enable type-safe reusable code
10. Decorators
- Class, method, property, and parameter decorators
- Detailed examples showing how decorators can add metadata and modify behavior
- Implementation of a logging decorator
11. Utility Types
- Comprehensive coverage of built-in utility types: Partial, Required, Readonly, Record, Pick, Omit, etc.
- Practical examples showing how each utility type transforms existing types
12. Modules and Namespaces
- ES modules with import/export syntax
- Namespaces for organizing code
- Default vs. named exports
- Nested namespaces
13. TypeScript with Frameworks
- React examples with typed props, state, and event handlers
- Angular examples with typed components, services, and modules
- Vue examples using the composition API with TypeScript
The tutorial concludes with a comprehensive practical example that combines multiple TypeScript features in a student management system.
========================================================================
9. Generics
Generics allow you to create reusable components that work with a variety of types:
// Generic function
function identity<T>(arg: T): T {
return arg;
}
// Explicit type parameter
let output1 = identity<string>("myString");
// Type inference
let output2 = identity(42); // Type 'number' is inferred
// Generic interface
interface GenericBox<T> {
value: T;
}
let numberBox: GenericBox<number> = { value: 42 };
let stringBox: GenericBox<string> = { value: "hello" };
// Generic classes
class DataContainer<T> {
private data: T[];
constructor() {
this.data = [];
}
add(item: T): void {
this.data.push(item);
}
getAll(): T[] {
return this.data;
}
}
// Usage
const numbers = new DataContainer<number>();
numbers.add(10);
numbers.add(20);
console.log(numbers.getAll()); // [10, 20]
// Generic constraints
interface HasLength {
length: number;
}
// T must have a length property
function logLength<T extends HasLength>(arg: T): T {
console.log(arg.length);
return arg;
}
logLength("hello"); // 5
logLength([1, 2, 3]); // 3
// logLength(42); // Error: number doesn't have a length property
10. Decorators
Decorators provide a way to add annotations and metadata to class declarations, methods, properties, and parameters:
// Enable experimental decorators in tsconfig.json:
// "experimentalDecorators": true
// Class decorator
function sealed(constructor: Function) {
Object.seal(constructor);
Object.seal(constructor.prototype);
}
// Method decorator
function log(target: any, propertyKey: string, descriptor: PropertyDescriptor) {
const originalMethod = descriptor.value;
descriptor.value = function(...args: any[]) {
console.log(`Calling ${propertyKey} with args: ${JSON.stringify(args)}`);
const result = originalMethod.apply(this, args);
console.log(`Method ${propertyKey} returned: ${JSON.stringify(result)}`);
return result;
};
return descriptor;
}
// Property decorator
function format(formatString: string) {
return function(target: any, propertyKey: string) {
let value: any;
const getter = function() {
return value;
};
const setter = function(newVal: any) {
value = formatString.replace("%s", newVal.toString());
};
Object.defineProperty(target, propertyKey, {
get: getter,
set: setter,
enumerable: true,
configurable: true
});
};
}
// Parameter decorator
function required(target: any, propertyKey: string, parameterIndex: number) {
const requiredParams: number[] = Reflect.getOwnMetadata("required", target, propertyKey) || [];
requiredParams.push(parameterIndex);
Reflect.defineMetadata("required", requiredParams, target, propertyKey);
}
// Using decorators
@sealed
class Person {
@format("Hello, %s!")
name: string;
constructor(name: string) {
this.name = name;
}
@log
greet(@required message: string): string {
return `${this.name} says: ${message}`;
}
}
const person = new Person("Alice");
console.log(person.name); // "Hello, Alice!"
person.greet("Hi everyone!");
// Logs:
// Calling greet with args: ["Hi everyone!"]
// Method greet returned: "Hello, Alice! says: Hi everyone!"
11. Utility Types
TypeScript provides several utility types to facilitate common type transformations:
// Base interface for examples
interface User {
id: number;
name: string;
email: string;
role: "admin" | "user";
address: {
street: string;
city: string;
};
}
// Partial<T> - Makes all properties optional
type PartialUser = Partial<User>;
const userUpdate: PartialUser = {
name: "New Name",
// Other fields are optional
};
// Required<T> - Makes all properties required
type StrictUser = Required<Partial<User>>;
// Now all properties are required again
// Readonly<T> - Makes all properties readonly
type ReadonlyUser = Readonly<User>;
const user: ReadonlyUser = {
id: 1,
name: "John",
email: "john@example.com",
role: "user",
address: {
street: "123 Main St",
city: "Anytown"
}
};
// user.name = "Jane"; // Error: Cannot assign to 'name' because it is a read-only property
// Record<K, T> - Construct a type with set of properties K of type T
type UserRoles = Record<string, "admin" | "editor" | "viewer">;
const roles: UserRoles = {
john: "admin",
jane: "editor",
bob: "viewer"
};
// Pick<T, K> - Pick a set of properties from T
type UserIdentity = Pick<User, "id" | "name">;
const identity: UserIdentity = {
id: 1,
name: "John"
};
// Omit<T, K> - Omit a set of properties from T
type UserWithoutSensitiveInfo = Omit<User, "email" | "address">;
const publicUser: UserWithoutSensitiveInfo = {
id: 1,
name: "John",
role: "user"
};
// Exclude<T, U> - Exclude types in U from T
type AdminOrUser = "admin" | "user" | "guest" | "editor";
type SystemRoles = Exclude<AdminOrUser, "guest" | "editor">;
// SystemRoles = "admin" | "user"
// Extract<T, U> - Extract types in U from T
type ExtractedRoles = Extract<AdminOrUser, "admin" | "editor" | "owner">;
// ExtractedRoles = "admin" | "editor"
// NonNullable<T> - Removes null and undefined from T
type MaybeString = string | null | undefined;
type DefinitelyString = NonNullable<MaybeString>;
// DefinitelyString = string
// ReturnType<T> - Get the return type of a function type
function createUser(name: string, role: "admin" | "user") {
return { id: Date.now(), name, role };
}
type CreateUserReturn = ReturnType<typeof createUser>;
// CreateUserReturn = { id: number; name: string; role: "admin" | "user"; }
// Parameters<T> - Get the parameter types of a function type as a tuple
type CreateUserParams = Parameters<typeof createUser>;
// CreateUserParams = [name: string, role: "admin" | "user"]
12. Modules and Namespaces
12.1 Modules
TypeScript modules allow you to organize code into separate files:
// 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 const PI = 3.14159;
// Default export
export default class Calculator {
add(x: number, y: number): number {
return x + y;
}
subtract(x: number, y: number): number {
return x - y;
}
}
// user.ts
export interface User {
id: number;
name: string;
}
export function createUser(name: string): User {
return { id: Date.now(), name };
}
// app.ts
import Calculator, { add, subtract, PI } from './math';
import * as UserModule from './user';
// Using named imports
console.log(add(5, 3)); // 8
console.log(PI); // 3.14159
// Using default import
const calc = new Calculator();
console.log(calc.add(10, 5)); // 15
// Using namespace import
const user = UserModule.createUser("Alice");
console.log(user); // { id: 1647352622222, name: "Alice" }
12.2 Namespaces
Namespaces (previously called "internal modules") provide another way to organize code:
// validation.ts
namespace Validation {
export interface StringValidator {
isValid(s: string): boolean;
}
export class ZipCodeValidator implements StringValidator {
isValid(s: string): boolean {
const zipRegex = /^\d{5}(-\d{4})?$/;
return zipRegex.test(s);
}
}
export class EmailValidator implements StringValidator {
isValid(s: string): boolean {
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
return emailRegex.test(s);
}
}
}
// Usage
const zipValidator = new Validation.ZipCodeValidator();
console.log(zipValidator.isValid("12345")); // true
const emailValidator = new Validation.EmailValidator();
console.log(emailValidator.isValid("test@example.com")); // true
// Nested namespaces
namespace App {
export namespace Utils {
export function format(str: string): string {
return str.trim().toLowerCase();
}
}
export namespace Models {
export interface User {
id: number;
name: string;
}
}
}
// Using nested namespace
console.log(App.Utils.format(" HELLO ")); // "hello"
13. TypeScript with Frameworks
13.1 React with TypeScript
// Basic component with props type
import React from 'react';
// Props interface
interface GreetingProps {
name: string;
age?: number;
onGreet: (name: string) => void;
}
// Functional component with typed props
const Greeting: React.FC<GreetingProps> = ({ name, age, onGreet }) => {
return (
<div>
<h1>Hello, {name}!</h1>
{age && <p>You are {age} years old.</p>}
<button onClick={() => onGreet(name)}>Greet</button>
</div>
);
};
// Usage
import React from 'react';
import ReactDOM from 'react-dom';
import { Greeting } from './Greeting';
function App() {
const handleGreet = (name: string) => {
alert(`Hello, ${name}!`);
};
return (
<div className="App">
<Greeting name="Alice" age={30} onGreet={handleGreet} />
</div>
);
}
ReactDOM.render(<App />, document.getElementById('root'));
// Using useState with type
import React, { useState } from 'react';
interface User {
id: number;
name: string;
email: string;
}
const UserProfile: React.FC = () => {
// Typed state
const [user, setUser] = useState<User | null>(null);
const [loading, setLoading] = useState<boolean>(true);
// Load user
React.useEffect(() => {
const fetchUser = async () => {
try {
setLoading(true);
const response = await fetch('https://api.example.com/user/1');
const data = await response.json();
setUser(data);
} catch (error) {
console.error('Error fetching user:', error);
} finally {
setLoading(false);
}
};
fetchUser();
}, []);
if (loading) return <p>Loading...</p>;
if (!user) return <p>No user found.</p>;
return (
<div>
<h1>{user.name}</h1>
<p>Email: {user.email}</p>
</div>
);
};
// Event handling with TypeScript
import React from 'react';
const Form: React.FC = () => {
const handleSubmit = (event: React.FormEvent<HTMLFormElement>) => {
event.preventDefault();
// Form submission logic
};
const handleInputChange = (event: React.ChangeEvent<HTMLInputElement>) => {
const { name, value } = event.target;
console.log(`Field ${name} changed to ${value}`);
};
const handleButtonClick = (event: React.MouseEvent<HTMLButtonElement>) => {
console.log('Button clicked', event.currentTarget);
};
return (
<form onSubmit={handleSubmit}>
<input
type="text"
name="username"
onChange={handleInputChange}
/>
<button type="button" onClick={handleButtonClick}>
Click Me
</button>
<button type="submit">Submit</button>
</form>
);
};
13.2 Angular with TypeScript
// Component example
import { Component, Input, Output, EventEmitter } from '@angular/core';
interface User {
id: number;
name: string;
email: string;
}
@Component({
selector: 'app-user-card',
template: `
<div class="user-card">
<h2>{{ user.name }}</h2>
<p>{{ user.email }}</p>
<button (click)="onSelect()">Select User</button>
</div>
`,
styleUrls: ['./user-card.component.css']
})
export class UserCardComponent {
@Input() user!: User;
@Output() selected = new EventEmitter<User>();
onSelect(): void {
this.selected.emit(this.user);
}
}
// Service example
import { Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { Observable } from 'rxjs';
import { User } from './user.model';
@Injectable({
providedIn: 'root'
})
export class UserService {
private apiUrl = 'https://api.example.com/users';
constructor(private http: HttpClient) {}
getUsers(): Observable<User[]> {
return this.http.get<User[]>(this.apiUrl);
}
getUserById(id: number): Observable<User> {
return this.http.get<User>(`${this.apiUrl}/${id}`);
}
createUser(user: Omit<User, 'id'>): Observable<User> {
return this.http.post<User>(this.apiUrl, user);
}
updateUser(user: User): Observable<User> {
return this.http.put<User>(`${this.apiUrl}/${user.id}`, user);
}
deleteUser(id: number): Observable<void> {
return this.http.delete<void>(`${this.apiUrl}/${id}`);
}
}
// Module example
import { NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import { HttpClientModule } from '@angular/common/http';
import { FormsModule, ReactiveFormsModule } from '@angular/forms';
import { AppComponent } from './app.component';
import { UserCardComponent } from './user-card/user-card.component';
import { UserListComponent } from './user-list/user-list.component';
@NgModule({
declarations: [
AppComponent,
UserCardComponent,
UserListComponent
],
imports: [
BrowserModule,
HttpClientModule,
FormsModule,
ReactiveFormsModule
],
providers: [],
bootstrap: [AppComponent]
})
export class AppModule { }
13.3 Vue with TypeScript
// Using Vue 3 with TypeScript
// Single-file component with TypeScript
<template>
<div class="user-profile">
<h1>{{ user.name }}</h1>
<p>{{ user.email }}</p>
<button @click="incrementCount">Count: {{ count }}</button>
</div>
</template>
<script lang="ts">
import { defineComponent, ref, PropType } from 'vue';
interface User {
id: number;
name: string;
email: string;
}
export default defineComponent({
name: 'UserProfile',
props: {
user: {
type: Object as PropType<User>,
required: true
}
},
setup() {
// Reactive state
const count = ref<number>(0);
// Methods
const incrementCount = () => {
count.value++;
};
return {
count,
incrementCount
};
}
});
</script>
// Using Vue 3 composition API with TypeScript
<script lang="ts">
import { defineComponent, ref, computed, onMounted, watch } from 'vue';
interface Task {
id: number;
title: string;
completed: boolean;
}
export default defineComponent({
name: 'TaskList',
props: {
filter: {
type: String,
default: 'all'
}
},
setup(props) {
// Reactive state
const tasks = ref<Task[]>([]);
const newTaskTitle = ref<string>('');
// Computed property
const filteredTasks = computed<Task[]>(() => {
switch (props.filter) {
case 'completed':
return tasks.value.filter(task => task.completed);
case 'active':
return tasks.value.filter(task => !task.completed);
default:
return tasks.value;
}
});
// Methods
const addTask = () => {
if (!newTaskTitle.value.trim()) return;
const newTask: Task = {
id: Date.now(),
title: newTaskTitle.value.trim(),
completed: false
};
tasks.value.push(newTask);
newTaskTitle.value = '';
};
const toggleTask = (task: Task) => {
task.completed = !task.completed;
};
const removeTask = (taskId: number) => {
tasks.value = tasks.value.filter(task => task.id !== taskId);
};
// Lifecycle hook
onMounted(() => {
// Load tasks from local storage
const savedTasks = localStorage.getItem('tasks');
if (savedTasks) {
tasks.value = JSON.parse(savedTasks);
}
});
// Watch for changes
watch(tasks, (newTasks) => {
// Save tasks to local storage
localStorage.setItem('tasks', JSON.stringify(newTasks));
}, { deep: true });
return {
tasks,
filteredTasks,
newTaskTitle,
addTask,
toggleTask,
removeTask
};
}
});
</script>
Practical Example
Here's a complete example combining several TypeScript features:
// student-management-system.ts
// --- Types ---
type StudentId = string | number;
enum CourseStatus {
NotStarted = "NOT_STARTED",
InProgress = "IN_PROGRESS",
Completed = "COMPLETED"
}
interface Course {
id: number;
name: string;
credits: number;
status: CourseStatus;
}
interface Person {
firstName: string;
lastName: string;
}
interface Student extends Person {
id: StudentId;
courses: Course[];
graduationYear?: number;
}
// --- Generic Repository ---
class Repository<T> {
private items: T[] = [];
add(item: T): void {
this.items.push(item);
}
getAll(): T[] {
return [...this.items];
}
find(predicate: (item: T) => boolean): T | undefined {
return this.items.find(predicate);
}
}
// --- Service ---
class StudentService {
private studentRepository: Repository<Student>;
constructor() {
this.studentRepository = new Repository<Student>();
}
addStudent(student: Student): void {
this.studentRepository.add(student);
}
getStudents(): Student[] {
return this.studentRepository.getAll();
}
getStudentById(id: StudentId): Student | undefined {
return this.studentRepository.find(student => student.id === id);
}
@log
calculateCompletedCredits(studentId: StudentId): number {
const student = this.getStudentById(studentId);
if (!student) return 0;
return student.courses
.filter(course => course.status === CourseStatus.Completed)
.reduce((total, course) => total + course.credits, 0);
}
}
// --- Decorator ---
function log(
target: any,
propertyKey: string,
descriptor: PropertyDescriptor
): PropertyDescriptor {
const originalMethod = descriptor.value;
descriptor.value = function(...args: any[]) {
console.log(`Calling ${propertyKey} with args: ${JSON.stringify(args)}`);
const result = originalMethod.apply(this, args);
console.log(`Method ${propertyKey} returned: ${result}`);
return result;
};
return descriptor;
}
// --- Utility Functions ---
function formatStudentName<T extends Person>(person: T): string {
return `${person.firstName} ${person.lastName}`;
}
// --- Sample Data ---
const courses: Course[] = [
{ id: 1, name: "Introduction to TypeScript", credits: 3, status: CourseStatus.Completed },
{ id: 2, name: "Advanced Programming", credits: 4, status: CourseStatus.InProgress },
{ id: 3, name: "Web Development", credits: 3, status: CourseStatus.NotStarted }
];
// --- Main Program ---
function main() {
const studentService = new StudentService();
// Add students
studentService.addStudent({
id: "CS101",
firstName: "Alice",
lastName: "Johnson",
courses: [courses[0], courses[1]],
graduationYear: 2025
});
studentService.addStudent({
id: 202,
firstName: "Bob",
lastName: "Smith",
courses: [courses[0], courses[2]]
});
// Display students
const students = studentService.getStudents();
students.forEach(student => {
console.log(`Student: ${formatStudentName(student)} (ID: ${student.id})`);
console.log(`Completed Credits: ${studentService.calculateCompletedCredits(student.id)}`);
console.log(`Expected Graduation: ${student.graduationYear ?? "Not specified"}`);
console.log("Courses:");
student.courses.forEach(course => {
console.log(`- ${course.name} (${course.credits} credits): ${course.status}`);
});
console.log("---");
});
}
main();
Next Steps
To further your TypeScript knowledge, consider exploring:
- Advanced Generic Patterns
- Conditional Types
- Mapped Types
- TypeScript Compiler API
- Testing TypeScript code with Jest/Mocha
- TypeScript with Node.js backend development
- Building and publishing TypeScript libraries