Skip to content
Code Kata
Go back

TypeScript Core Concepts

Suggest edit

Table of contents

Open Table of contents

Overview

This post covers the foundational TypeScript concepts that appear throughout the kata exercises. It’s designed as a reference you can revisit as you work through data structure, design pattern, and algorithm katas.

The Type System

TypeScript uses a structural type system — types are compatible if their shapes match, regardless of explicit declarations.

interface Point {
  x: number;
  y: number;
}

// No explicit `implements Point`, but this works
const p = { x: 10, y: 20 };
const point: Point = p; // OK

Literal Types

Narrow types to specific values:

type Direction = "north" | "south" | "east" | "west";
type HttpStatus = 200 | 404 | 500;

Type Assertions vs Type Guards

Assertions (as) override the compiler. Guards (is, typeof, instanceof, in) refine types safely.

// Assertion -- you promise the compiler
const input = getInput() as string;

// Guard -- the compiler verifies
function isString(value: unknown): value is string {
  return typeof value === "string";
}

Generics

Generics enable reusable, type-safe abstractions — critical for data structure katas.

class Stack<T> {
  private items: T[] = [];

  push(item: T): void {
    this.items.push(item);
  }

  pop(): T | undefined {
    return this.items.pop();
  }
}

const numStack = new Stack<number>();
numStack.push(42);

Constraints

Restrict what types a generic can accept:

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

Generic Defaults

interface ApiResponse<T = unknown> {
  data: T;
  status: number;
}

Utility Types

Built-in types for common transformations:

TypeDescriptionExample
Partial<T>All properties optionalConfig overrides
Required<T>All properties requiredValidation
Readonly<T>All properties readonlyImmutable data
Pick<T, K>Subset of propertiesAPI responses
Omit<T, K>Exclude propertiesForm data without ID
Record<K, V>Key-value mapRecord<string, number>
ReturnType<F>Return type of functionInfer from existing code
interface User {
  id: number;
  name: string;
  email: string;
  role: "admin" | "user";
}

type CreateUser = Omit<User, "id">;
type UserUpdate = Partial<Pick<User, "name" | "email">>;

Discriminated Unions

Combine union types with a shared literal property for exhaustive type narrowing:

type Shape =
  | { kind: "circle"; radius: number }
  | { kind: "rectangle"; width: number; height: number }
  | { kind: "triangle"; base: number; height: number };

function area(shape: Shape): number {
  switch (shape.kind) {
    case "circle":
      return Math.PI * shape.radius ** 2;
    case "rectangle":
      return shape.width * shape.height;
    case "triangle":
      return (shape.base * shape.height) / 2;
  }
}

Exhaustiveness Checking

Use never to ensure all variants are handled:

function assertNever(x: never): never {
  throw new Error(`Unexpected value: ${x}`);
}

function area(shape: Shape): number {
  switch (shape.kind) {
    case "circle":
      return Math.PI * shape.radius ** 2;
    case "rectangle":
      return shape.width * shape.height;
    case "triangle":
      return (shape.base * shape.height) / 2;
    default:
      return assertNever(shape); // compile error if a variant is missed
  }
}

Mapped Types

Transform existing types programmatically:

type Flags<T> = {
  [K in keyof T]: boolean;
};

interface Features {
  darkMode: string;
  notifications: string;
}

type FeatureFlags = Flags<Features>;
// { darkMode: boolean; notifications: boolean }

Key Remapping

type Getters<T> = {
  [K in keyof T as `get${Capitalize<string & K>}`]: () => T[K];
};

Conditional Types

Types that depend on conditions:

type IsArray<T> = T extends any[] ? true : false;

type A = IsArray<string[]>; // true
type B = IsArray<number>; // false

infer Keyword

Extract types from within other types:

type ElementType<T> = T extends (infer E)[] ? E : never;

type A = ElementType<string[]>; // string
type B = ElementType<number>; // never
type UnwrapPromise<T> = T extends Promise<infer U> ? U : T;

type A = UnwrapPromise<Promise<string>>; // string
type B = UnwrapPromise<number>; // number

Template Literal Types

Build types from string patterns:

type EventName = `on${Capitalize<"click" | "focus" | "blur">}`;
// "onClick" | "onFocus" | "onBlur"

type CssProperty = `${string}-${string}`;

Declaration Merging

Interfaces with the same name merge automatically:

interface Window {
  myCustomProperty: string;
}

// Now window.myCustomProperty is typed

Module Patterns

Barrel Exports

// utils/index.ts
export { slugify } from "./slugify";
export { formatDate } from "./date";
export type { PostFilter } from "./types";

Type-Only Imports

import type { User } from "./models";

Key Takeaways

  1. Structural typing — shape matters, not names
  2. Generics — the backbone of reusable data structures
  3. Discriminated unions — safe, exhaustive pattern matching
  4. Utility types — avoid rewriting common transformations
  5. Mapped + conditional types — build complex types from simple ones

Suggest edit
Share this post on:

Previous Post
PHP 8.x: Key Updates by Version
Next Post
TypeScript 5.x: Key Updates by Version