Wednesday, March 12, 2025

#2 Extended TS Tutorial

 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:

  1. Advanced Generic Patterns
  2. Conditional Types
  3. Mapped Types
  4. TypeScript Compiler API
  5. Testing TypeScript code with Jest/Mocha
  6. TypeScript with Node.js backend development
  7. Building and publishing TypeScript libraries


#1 Type Script Tutorial


TypeScript Tutorial for Undergraduate Students

1. Built-in Types

TypeScript provides several primitive types that JavaScript developers are familiar with:

// Basic types
let isDone: boolean = false;
let decimal: number = 6;
let color: string = "blue";
let bigNumber: bigint = 100n;
let notDefined: undefined = undefined;
let absent: null = null;
let sym: symbol = Symbol("unique");

2. any Type

The any type allows you to work with dynamic content or migrate from JavaScript:

let notSure: any = 4;
notSure = "maybe a string";
notSure = false; // Also OK

// any allows access to any property or method without type checking
notSure.toFixed(); // No error during compilation

3. Arrays

Arrays can be typed in two ways:

// Using square brackets syntax
let list1: number[] = [1, 2, 3];

// Using generic Array type
let list2: Array<number> = [1, 2, 3];

// Mixed type arrays
let mixed: (string | number)[] = ["hello", 42, "world"];

4. Tuples

Tuples allow you to express an array with fixed number of elements whose types are known:

// Tuple type definition
let x: [string, number];

// Initialization
x = ["hello", 10]; // OK
// x = [10, "hello"]; // Error: incorrect order

// Accessing tuple elements with correct types
console.log(x[0].substring(1)); // OK
// console.log(x[1].substring(1)); // Error: 'number' has no 'substring' method

5. Enums

Enums provide a way to define a set of named constants:

// Numeric enum
enum Color {
  Red,     // 0
  Green,   // 1
  Blue     // 2
}
let c: Color = Color.Green;
console.log(c); // 1

// String enum
enum Direction {
  Up = "UP",
  Down = "DOWN",
  Left = "LEFT",
  Right = "RIGHT"
}
let dir: Direction = Direction.Up;
console.log(dir); // "UP"

6. Functions

TypeScript allows you to specify parameter and return types for functions:

// Function with parameter types and return type
function add(x: number, y: number): number {
  return x + y;
}

// Optional parameters
function buildName(firstName: string, lastName?: string): string {
  if (lastName) {
    return `${firstName} ${lastName}`;
  }
  return firstName;
}

// Default parameters
function greet(name: string, greeting: string = "Hello"): string {
  return `${greeting}, ${name}!`;
}

// Function types
let myAdd: (x: number, y: number) => number = add;

7. Objects

Objects can be typed using interfaces or type aliases:

// Object type with interface
interface Person {
  firstName: string;
  lastName: string;
  age: number;
}

// Using the interface
let user: Person = {
  firstName: "John",
  lastName: "Doe",
  age: 30
};

// Inline object type annotation
let employee: { id: number; name: string } = {
  id: 100,
  name: "Alice"
};

8. Advanced Types

8.1 Union Types

Union types allow a value to be one of several types:

// Variable that can be either string or number
let id: string | number;
id = 101;  // OK
id = "202"; // OK
// id = true; // Error: Type 'boolean' not assignable

// Using union in function parameters
function printId(id: number | string) {
  console.log(`ID: ${id}`);
}

8.2 Type Aliases

Type aliases create a new name for a type:

// Simple type alias
type Point = {
  x: number;
  y: number;
};

// Using the type alias
const p: Point = { x: 10, y: 20 };

// Aliasing union types
type ID = string | number;
let studentId: ID = "A123";

8.3 Intersection Types

Intersection types combine multiple types into one:

// Two interfaces
interface HasName {
  name: string;
}

interface HasAge {
  age: number;
}

// Intersection type
type Person = HasName & HasAge;

// Using intersection type
let employee: Person = {
  name: "Alice",
  age: 30
};

8.4 Literal Types

Literal types allow you to specify exact values a variable can have:

// String literal type
type Direction = "up" | "down" | "left" | "right";
let movement: Direction = "up"; // OK
// let invalid: Direction = "sideways"; // Error

// Numeric literal type
type DiceRoll = 1 | 2 | 3 | 4 | 5 | 6;
let roll: DiceRoll = 6; // OK
// let invalidRoll: DiceRoll = 7; // Error

8.5 Nullable Types

TypeScript handles null and undefined with the strictNullChecks compiler option:

// With strictNullChecks enabled
let name: string;
// name = null; // Error with strictNullChecks
// name = undefined; // Error with strictNullChecks

// Explicitly allowing null
let nullableName: string | null = "Alice";
nullableName = null; // OK

// Checking for null before using
function printName(name: string | null) {
  if (name === null) {
    console.log("Name not provided");
  } else {
    console.log(`Name: ${name}`);
  }
}

8.6 Optional Properties

Optional properties are marked with a question mark:

// Interface with optional properties
interface Config {
  color?: string;
  width?: number;
}

// Both valid
const config1: Config = {};
const config2: Config = { color: "red", width: 100 };

// Optional chaining
console.log(config1.color?.toUpperCase()); // No error, returns undefined

Practical Example

Here's a complete example combining several TypeScript features:

// Student management system

// Define 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;
}

// Create student records
const students: Student[] = [
  {
    id: "CS101",
    firstName: "Alice",
    lastName: "Johnson",
    courses: [
      { 
        id: 1, 
        name: "Introduction to TypeScript", 
        credits: 3, 
        status: CourseStatus.Completed 
      },
      { 
        id: 2, 
        name: "Advanced Programming", 
        credits: 4, 
        status: CourseStatus.InProgress 
      }
    ],
    graduationYear: 2025
  },
  {
    id: 202,
    firstName: "Bob",
    lastName: "Smith",
    courses: [
      { 
        id: 1, 
        name: "Introduction to TypeScript", 
        credits: 3, 
        status: CourseStatus.InProgress 
      }
    ]
  }
];

// Function to calculate GPA (simplified)
function calculateCredits(student: Student): number {
  return student.courses
    .filter(course => course.status === CourseStatus.Completed)
    .reduce((total, course) => total + course.credits, 0);
}

// Print student information
function printStudentInfo(student: Student): void {
  console.log(`Student: ${student.firstName} ${student.lastName} (ID: ${student.id})`);
  console.log(`Completed Credits: ${calculateCredits(student)}`);
  console.log(`Expected Graduation: ${student.graduationYear ?? "Not specified"}`);
  console.log("Courses:");
  
  student.courses.forEach(course => {
    console.log(`- ${course.name} (${course.credits} credits): ${course.status}`);
  });
}

// Use the functions
students.forEach(printStudentInfo);

Next Steps

To deepen your TypeScript knowledge, explore:

  1. Generics
  2. Decorators
  3. Utility Types (Pick, Omit, Partial, etc.)
  4. Modules and Namespaces
  5. TypeScript with Frameworks (React, Angular, Vue)

I've created a comprehensive TypeScript tutorial for undergraduate students covering all the topics you requested. The tutorial includes:

  1. Built-in primitive types (boolean, number, string, etc.)
  2. The any type for flexibility
  3. Arrays with different typing approaches
  4. Tuples for fixed-length arrays with specific types
  5. Enums for named constants
  6. Functions with parameter and return types
  7. Object typing with interfaces
  8. Advanced types including:
    • Union types
    • Type aliases
    • Intersection types
    • Literal types
    • Nullable types
    • Optional properties

The tutorial provides code examples for each concept and concludes with a practical student management system example that combines multiple TypeScript features. It also suggests next steps for students who want to deepen their TypeScript knowledge.

<><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><>


TypeScript Tutorial for Undergraduate Students

1. Built-in Types

TypeScript provides several primitive types that JavaScript developers are familiar with:

// Basic types
let isDone: boolean = false;
let decimal: number = 6;
let color: string = "blue";
let bigNumber: bigint = 100n;
let notDefined: undefined = undefined;
let absent: null = null;
let sym: symbol = Symbol("unique");

2. any Type

The any type allows you to work with dynamic content or migrate from JavaScript:

let notSure: any = 4;
notSure = "maybe a string";
notSure = false; // Also OK

// any allows access to any property or method without type checking
notSure.toFixed(); // No error during compilation

3. Arrays

Arrays can be typed in two ways:

// Using square brackets syntax
let list1: number[] = [1, 2, 3];

// Using generic Array type
let list2: Array<number> = [1, 2, 3];

// Mixed type arrays
let mixed: (string | number)[] = ["hello", 42, "world"];

4. Tuples

Tuples allow you to express an array with fixed number of elements whose types are known:

// Tuple type definition
let x: [string, number];

// Initialization
x = ["hello", 10]; // OK
// x = [10, "hello"]; // Error: incorrect order

// Accessing tuple elements with correct types
console.log(x[0].substring(1)); // OK
// console.log(x[1].substring(1)); // Error: 'number' has no 'substring' method

5. Enums

Enums provide a way to define a set of named constants:

// Numeric enum
enum Color {
  Red,     // 0
  Green,   // 1
  Blue     // 2
}
let c: Color = Color.Green;
console.log(c); // 1

// String enum
enum Direction {
  Up = "UP",
  Down = "DOWN",
  Left = "LEFT",
  Right = "RIGHT"
}
let dir: Direction = Direction.Up;
console.log(dir); // "UP"

6. Functions

TypeScript allows you to specify parameter and return types for functions:

// Function with parameter types and return type
function add(x: number, y: number): number {
  return x + y;
}

// Optional parameters
function buildName(firstName: string, lastName?: string): string {
  if (lastName) {
    return `${firstName} ${lastName}`;
  }
  return firstName;
}

// Default parameters
function greet(name: string, greeting: string = "Hello"): string {
  return `${greeting}, ${name}!`;
}

// Function types
let myAdd: (x: number, y: number) => number = add;

7. Objects

Objects can be typed using interfaces or type aliases:

// Object type with interface
interface Person {
  firstName: string;
  lastName: string;
  age: number;
}

// Using the interface
let user: Person = {
  firstName: "John",
  lastName: "Doe",
  age: 30
};

// Inline object type annotation
let employee: { id: number; name: string } = {
  id: 100,
  name: "Alice"
};

8. Advanced Types

8.1 Union Types

Union types allow a value to be one of several types:

// Variable that can be either string or number
let id: string | number;
id = 101;  // OK
id = "202"; // OK
// id = true; // Error: Type 'boolean' not assignable

// Using union in function parameters
function printId(id: number | string) {
  console.log(`ID: ${id}`);
}

8.2 Type Aliases

Type aliases create a new name for a type:

// Simple type alias
type Point = {
  x: number;
  y: number;
};

// Using the type alias
const p: Point = { x: 10, y: 20 };

// Aliasing union types
type ID = string | number;
let studentId: ID = "A123";

8.3 Intersection Types

Intersection types combine multiple types into one:

// Two interfaces
interface HasName {
  name: string;
}

interface HasAge {
  age: number;
}

// Intersection type
type Person = HasName & HasAge;

// Using intersection type
let employee: Person = {
  name: "Alice",
  age: 30
};

8.4 Literal Types

Literal types allow you to specify exact values a variable can have:

// String literal type
type Direction = "up" | "down" | "left" | "right";
let movement: Direction = "up"; // OK
// let invalid: Direction = "sideways"; // Error

// Numeric literal type
type DiceRoll = 1 | 2 | 3 | 4 | 5 | 6;
let roll: DiceRoll = 6; // OK
// let invalidRoll: DiceRoll = 7; // Error

8.5 Nullable Types

TypeScript handles null and undefined with the strictNullChecks compiler option:

// With strictNullChecks enabled
let name: string;
// name = null; // Error with strictNullChecks
// name = undefined; // Error with strictNullChecks

// Explicitly allowing null
let nullableName: string | null = "Alice";
nullableName = null; // OK

// Checking for null before using
function printName(name: string | null) {
  if (name === null) {
    console.log("Name not provided");
  } else {
    console.log(`Name: ${name}`);
  }
}

8.6 Optional Properties

Optional properties are marked with a question mark:

// Interface with optional properties
interface Config {
  color?: string;
  width?: number;
}

// Both valid
const config1: Config = {};
const config2: Config = { color: "red", width: 100 };

// Optional chaining
console.log(config1.color?.toUpperCase()); // No error, returns undefined

Practical Example

Here's a complete example combining several TypeScript features:

// Student management system

// Define 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;
}

// Create student records
const students: Student[] = [
  {
    id: "CS101",
    firstName: "Alice",
    lastName: "Johnson",
    courses: [
      { 
        id: 1, 
        name: "Introduction to TypeScript", 
        credits: 3, 
        status: CourseStatus.Completed 
      },
      { 
        id: 2, 
        name: "Advanced Programming", 
        credits: 4, 
        status: CourseStatus.InProgress 
      }
    ],
    graduationYear: 2025
  },
  {
    id: 202,
    firstName: "Bob",
    lastName: "Smith",
    courses: [
      { 
        id: 1, 
        name: "Introduction to TypeScript", 
        credits: 3, 
        status: CourseStatus.InProgress 
      }
    ]
  }
];

// Function to calculate GPA (simplified)
function calculateCredits(student: Student): number {
  return student.courses
    .filter(course => course.status === CourseStatus.Completed)
    .reduce((total, course) => total + course.credits, 0);
}

// Print student information
function printStudentInfo(student: Student): void {
  console.log(`Student: ${student.firstName} ${student.lastName} (ID: ${student.id})`);
  console.log(`Completed Credits: ${calculateCredits(student)}`);
  console.log(`Expected Graduation: ${student.graduationYear ?? "Not specified"}`);
  console.log("Courses:");
  
  student.courses.forEach(course => {
    console.log(`- ${course.name} (${course.credits} credits): ${course.status}`);
  });
}

// Use the functions
students.forEach(printStudentInfo);

Next Steps

To deepen your TypeScript knowledge, explore:

  1. Generics
  2. Decorators
  3. Utility Types (Pick, Omit, Partial, etc.)
  4. Modules and Namespaces
  5. TypeScript with Frameworks (React, Angular, Vue)

#2 Extended TS Tutorial

 I've extended the TypeScript tutorial with all the requested topics: 9. Generics Basic generic functions, interfaces, and classes T...