TypeScript Introduction - Type-Safe JavaScript Development

beginner | 50 min read | 2024.12.16

What You’ll Learn in This Tutorial

✓ TypeScript basic types
✓ How type inference works
✓ Interfaces and type aliases
✓ Union types and literal types
✓ Generics basics
✓ Practical type definition patterns

Prerequisites

  • Basic JavaScript knowledge
  • Node.js installed

Project Setup

# Create project directory
mkdir typescript-tutorial
cd typescript-tutorial

# Create package.json
npm init -y

# Install TypeScript
npm install -D typescript ts-node @types/node

# Create tsconfig.json
npx tsc --init

tsconfig.json Configuration

{
  "compilerOptions": {
    "target": "ES2020",
    "module": "commonjs",
    "strict": true,
    "esModuleInterop": true,
    "skipLibCheck": true,
    "forceConsistentCasingInFileNames": true,
    "outDir": "./dist"
  },
  "include": ["src/**/*"]
}

Step 1: Basic Types

Primitive Types

// src/01-primitives.ts

// String
let name: string = "John";
let greeting: string = `Hello, ${name}`;

// Number
let age: number = 25;
let price: number = 1980.5;
let hex: number = 0xff;

// Boolean
let isActive: boolean = true;
let hasPermission: boolean = false;

// null and undefined
let nothing: null = null;
let notDefined: undefined = undefined;

// any (disables type checking - avoid when possible)
let anything: any = "string";
anything = 123;
anything = true;

// unknown (safer than any)
let unknownValue: unknown = "hello";
// unknownValue.toUpperCase(); // Error
if (typeof unknownValue === "string") {
  unknownValue.toUpperCase(); // OK
}

Arrays and Tuples

// src/02-arrays.ts

// Arrays
let numbers: number[] = [1, 2, 3, 4, 5];
let names: string[] = ["Alice", "Bob", "Charlie"];

// Generic notation
let values: Array<number> = [10, 20, 30];

// Tuples (fixed length and fixed type arrays)
let tuple: [string, number] = ["age", 25];
let rgb: [number, number, number] = [255, 128, 0];

// Labeled tuples
let user: [name: string, age: number] = ["John", 30];

// Readonly arrays
let readonlyNumbers: readonly number[] = [1, 2, 3];
// readonlyNumbers.push(4); // Error

Object Types

// src/03-objects.ts

// Object type definition
let person: { name: string; age: number } = {
  name: "Jane",
  age: 28
};

// Optional properties
let config: { host: string; port?: number } = {
  host: "localhost"
  // port is optional
};

// Readonly properties
let point: { readonly x: number; readonly y: number } = {
  x: 10,
  y: 20
};
// point.x = 30; // Error

// Index signatures
let dictionary: { [key: string]: string } = {
  hello: "hello",
  goodbye: "goodbye"
};
dictionary["thanks"] = "thanks"; // OK

Step 2: Function Types

Basic Function Types

// src/04-functions.ts

// Parameter and return types
function add(a: number, b: number): number {
  return a + b;
}

// Arrow functions
const multiply = (a: number, b: number): number => a * b;

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

// Default parameters
function createUser(
  name: string,
  age: number = 20,
  role: string = "user"
): { name: string; age: number; role: string } {
  return { name, age, role };
}

// Rest parameters
function sum(...numbers: number[]): number {
  return numbers.reduce((acc, curr) => acc + curr, 0);
}

console.log(sum(1, 2, 3, 4, 5)); // 15

Function Type Definitions

// Function type variable
let calculator: (a: number, b: number) => number;
calculator = (x, y) => x + y;

// Function type with type alias
type MathOperation = (a: number, b: number) => number;

const subtract: MathOperation = (a, b) => a - b;
const divide: MathOperation = (a, b) => a / b;

// Callback functions
function processArray(
  arr: number[],
  callback: (item: number) => number
): number[] {
  return arr.map(callback);
}

const doubled = processArray([1, 2, 3], (x) => x * 2);
console.log(doubled); // [2, 4, 6]

void and never

// void - no return value
function logMessage(message: string): void {
  console.log(message);
}

// never - never returns
function throwError(message: string): never {
  throw new Error(message);
}

function infiniteLoop(): never {
  while (true) {}
}

Step 3: Type Aliases and Interfaces

Type Aliases (type)

// src/05-type-alias.ts

// Basic type aliases
type ID = string | number;
type Point = { x: number; y: number };

// Union types
type Status = "pending" | "approved" | "rejected";
type Result = string | null;

let userId: ID = "user_123";
let orderId: ID = 456;
let currentStatus: Status = "pending";

// Intersection types
type Named = { name: string };
type Aged = { age: number };
type Person = Named & Aged;

const person: Person = {
  name: "John",
  age: 30
};

Interfaces

// src/06-interface.ts

// Basic interface
interface User {
  id: number;
  name: string;
  email: string;
  age?: number; // Optional
  readonly createdAt: Date; // Readonly
}

const user: User = {
  id: 1,
  name: "Alice",
  email: "alice@example.com",
  createdAt: new Date()
};

// Interface inheritance
interface Employee extends User {
  department: string;
  salary: number;
}

const employee: Employee = {
  id: 2,
  name: "Bob",
  email: "bob@example.com",
  createdAt: new Date(),
  department: "Engineering",
  salary: 500000
};

// Interface with methods
interface Calculator {
  add(a: number, b: number): number;
  subtract(a: number, b: number): number;
}

const calc: Calculator = {
  add: (a, b) => a + b,
  subtract: (a, b) => a - b
};

Difference between type and interface

// type: can be used for unions, primitives, tuples
type StringOrNumber = string | number;
type Tuple = [string, number];

// interface: can be extended (declarations with same name auto-merge)
interface Window {
  title: string;
}
interface Window {
  size: { width: number; height: number };
}
// → Window has both properties

// General usage guidelines
// - Object shape definition → interface
// - Union types, primitives → type
// - Function types → type

Step 4: Union Types and Literal Types

Union Types

// src/07-union.ts

// Basic union type
type StringOrNumber = string | number;

function printId(id: StringOrNumber) {
  if (typeof id === "string") {
    console.log(id.toUpperCase());
  } else {
    console.log(id.toFixed(2));
  }
}

// Array union types
type MixedArray = (string | number)[];
const mixed: MixedArray = [1, "two", 3, "four"];

Literal Types

// String literal types
type Direction = "north" | "south" | "east" | "west";

function move(direction: Direction) {
  console.log(`Moving ${direction}`);
}

move("north"); // OK
// move("up"); // Error

// Number literal types
type DiceValue = 1 | 2 | 3 | 4 | 5 | 6;

function rollDice(): DiceValue {
  return Math.ceil(Math.random() * 6) as DiceValue;
}

// Boolean literal types
type True = true;

Discriminated Union Types

// src/08-discriminated-union.ts

interface Circle {
  kind: "circle";
  radius: number;
}

interface Square {
  kind: "square";
  sideLength: number;
}

interface Rectangle {
  kind: "rectangle";
  width: number;
  height: number;
}

type Shape = Circle | Square | Rectangle;

function calculateArea(shape: Shape): number {
  switch (shape.kind) {
    case "circle":
      return Math.PI * shape.radius ** 2;
    case "square":
      return shape.sideLength ** 2;
    case "rectangle":
      return shape.width * shape.height;
  }
}

const circle: Circle = { kind: "circle", radius: 5 };
console.log(calculateArea(circle)); // 78.54...

Step 5: Generics

Basic Generics

// src/09-generics.ts

// Generic function
function identity<T>(value: T): T {
  return value;
}

const str = identity<string>("hello");
const num = identity(42); // Type inference: number

// Generic function with arrays
function first<T>(arr: T[]): T | undefined {
  return arr[0];
}

const firstNumber = first([1, 2, 3]); // number | undefined
const firstString = first(["a", "b", "c"]); // string | undefined

Generic Interfaces

// API response type
interface ApiResponse<T> {
  data: T;
  status: number;
  message: string;
}

interface User {
  id: number;
  name: string;
}

interface Product {
  id: number;
  title: string;
  price: number;
}

const userResponse: ApiResponse<User> = {
  data: { id: 1, name: "Alice" },
  status: 200,
  message: "Success"
};

const productResponse: ApiResponse<Product[]> = {
  data: [
    { id: 1, title: "Item 1", price: 1000 },
    { id: 2, title: "Item 2", price: 2000 }
  ],
  status: 200,
  message: "Success"
};

Constrained Generics

// Add type constraint with extends
interface Lengthwise {
  length: number;
}

function logLength<T extends Lengthwise>(value: T): void {
  console.log(value.length);
}

logLength("hello"); // OK: string has length
logLength([1, 2, 3]); // OK: array has length
// logLength(123); // Error: number doesn't have length

// Constraint with keyof
function getProperty<T, K extends keyof T>(obj: T, key: K): T[K] {
  return obj[key];
}

const person = { name: "John", age: 30 };
const name = getProperty(person, "name"); // string
const age = getProperty(person, "age"); // number
// getProperty(person, "email"); // Error

Step 6: Utility Types

Built-in Utility Types

// src/10-utility-types.ts

interface User {
  id: number;
  name: string;
  email: string;
  age: number;
}

// Partial - make all optional
type PartialUser = Partial<User>;
const updateData: PartialUser = { name: "New Name" };

// Required - make all required
type RequiredUser = Required<User>;

// Pick - select specific properties
type UserBasic = Pick<User, "id" | "name">;
const basic: UserBasic = { id: 1, name: "Alice" };

// Omit - exclude specific properties
type UserWithoutEmail = Omit<User, "email">;

// Readonly - make all readonly
type ReadonlyUser = Readonly<User>;

// Record - object with specified key and value types
type UserRoles = Record<string, "admin" | "user" | "guest">;
const roles: UserRoles = {
  alice: "admin",
  bob: "user"
};

Practical Usage Examples

// Form state management
interface FormData {
  username: string;
  email: string;
  password: string;
}

// Form error state
type FormErrors = Partial<Record<keyof FormData, string>>;

const errors: FormErrors = {
  email: "Please enter a valid email address"
};

// API request/response
type CreateUserRequest = Omit<User, "id">;
type UpdateUserRequest = Partial<Omit<User, "id">>;
type UserResponse = Readonly<User>;

function createUser(data: CreateUserRequest): UserResponse {
  return { id: Date.now(), ...data } as UserResponse;
}

Practical Exercise: Type-Safe Todo App

// src/todo-app.ts

// Type definitions
type TodoStatus = "pending" | "in_progress" | "completed";
type Priority = "low" | "medium" | "high";

interface Todo {
  id: number;
  title: string;
  description?: string;
  status: TodoStatus;
  priority: Priority;
  createdAt: Date;
  completedAt?: Date;
}

type CreateTodoInput = Omit<Todo, "id" | "createdAt" | "completedAt">;
type UpdateTodoInput = Partial<Omit<Todo, "id" | "createdAt">>;

// TodoList class
class TodoList {
  private todos: Todo[] = [];
  private nextId = 1;

  add(input: CreateTodoInput): Todo {
    const todo: Todo = {
      ...input,
      id: this.nextId++,
      createdAt: new Date()
    };
    this.todos.push(todo);
    return todo;
  }

  update(id: number, input: UpdateTodoInput): Todo | null {
    const index = this.todos.findIndex(t => t.id === id);
    if (index === -1) return null;

    const updated = { ...this.todos[index], ...input };
    if (input.status === "completed" && !updated.completedAt) {
      updated.completedAt = new Date();
    }
    this.todos[index] = updated;
    return updated;
  }

  delete(id: number): boolean {
    const index = this.todos.findIndex(t => t.id === id);
    if (index === -1) return false;
    this.todos.splice(index, 1);
    return true;
  }

  getAll(): Readonly<Todo[]> {
    return this.todos;
  }

  getByStatus(status: TodoStatus): Todo[] {
    return this.todos.filter(t => t.status === status);
  }

  getByPriority(priority: Priority): Todo[] {
    return this.todos.filter(t => t.priority === priority);
  }
}

// Usage example
const todoList = new TodoList();

todoList.add({
  title: "Learn TypeScript",
  description: "Understand the basic type system",
  status: "in_progress",
  priority: "high"
});

todoList.add({
  title: "Learn React",
  status: "pending",
  priority: "medium"
});

console.log(todoList.getAll());
console.log(todoList.getByStatus("pending"));

Best Practices

1. Leverage Type Inference
   - Only add explicit type annotations where needed
   - Function return types are good to annotate

2. Avoid any
   - Use unknown and perform type checking
   - Consider generics when type is uncertain

3. Enable Strict Mode
   - Set "strict": true in tsconfig.json
   - Enables safer code

4. Choose Appropriate Type Definitions
   - Objects → interface
   - Unions, tuples → type

5. Utilize Utility Types
   - Derive new types from existing types
   - Avoid duplication and improve maintainability

Summary

By leveraging TypeScript’s type system, you can detect errors at compile time and write safer code. Start with basic types and work your way up to mastering generics and utility types.

← Back to list