Python

🎯 TypeScript Advanced Types: Union, Intersection & Mapped Types

Pelajari advanced type system TypeScript — union types, intersection types, conditional types, mapped types, dan template literal types

1. Union Types Lanjutan

Union types memungkinkan sebuah nilai memiliki beberapa tipe. Di level lanjutan, kita bisa menggunakan union types dengan lebih canggih untuk memodelkan data yang kompleks.

Narrowing & Type Guards

TypeScript — Union Narrowing
// Union type dasar
type StringOrNumber = string | number;

function proses(value: StringOrNumber): string {
  // Narrowing dengan typeof
  if (typeof value === "string") {
    return value.toUpperCase();  // TypeScript tahu ini string
  }
  return value.toFixed(2);       // TypeScript tahu ini number
}

console.log(proses("hello"));   // "HELLO"
console.log(proses(3.14159));   // "3.14"

// Complex union — banyak tipe
type Result = 
  | { status: "success"; data: any[] }
  | { status: "error"; message: string }
  | { status: "loading" };

function handleResult(result: Result): void {
  switch (result.status) {
    case "success":
      console.log(`Data: ${result.data.length} items`);
      break;
    case "error":
      console.log(`Error: ${result.message}`);
      break;
    case "loading":
      console.log("Loading...");
      break;
  }
}

// Exhaustive checking — memastikan semua case ditangani
function assertNever(value: never): never {
  throw new Error(`Unexpected value: ${value}`);
}

type Warna = "merah" | "hijau" | "biru";

function getHexCode(warna: Warna): string {
  switch (warna) {
    case "merah": return "#FF0000";
    case "hijau": return "#00FF00";
    case "biru": return "#0000FF";
    default: return assertNever(warna);
  }
}

Union dengan Literal Types

TypeScript — Literal Unions
// String literal union untuk event handling
type FormEvent = "submit" | "reset" | "change" | "focus" | "blur";

function handleFormEvent(event: FormEvent): void {
  console.log(`Form event: ${event}`);
}

handleFormEvent("submit");   // ✅
// handleFormEvent("click"); // ❌ Error: 'click' not in FormEvent

// Numeric literal union
type HttpStatus = 200 | 201 | 400 | 401 | 403 | 404 | 500;

function getMessage(code: HttpStatus): string {
  const messages: Record<HttpStatus, string> = {
    200: "OK",
    201: "Created",
    400: "Bad Request",
    401: "Unauthorized",
    403: "Forbidden",
    404: "Not Found",
    500: "Internal Server Error"
  };
  return messages[code];
}

// Boolean literal — lebih spesifik dari boolean
type Active = true;
type Inactive = false;

// Template literal union
type EventName = `on${Capitalize<'click' | 'hover' | 'focus'>}`;
// "onClick" | "onHover" | "onFocus"

2. Intersection Types

Intersection types menggabungkan beberapa tipe menjadi satu. Object dari intersection type harus memenuhi semua tipe yang digabungkan.

TypeScript — Intersection Types
// Intersection type — gabungkan beberapa tipe
type Printable = {
  print(): string;
};

type Loggable = {
  log(message: string): void;
};

type Serializable = {
  serialize(): string;
  deserialize(data: string): void;
};

// Object harus memenuhi SEMUA tipe
type PrintableLoggable = Printable & Loggable;
type FullFeatured = Printable & Loggable & Serializable;

class AppLogger implements FullFeatured {
  private logs: string[] = [];

  print(): string {
    return this.logs.join("\n");
  }

  log(message: string): void {
    const timestamp = new Date().toISOString();
    this.logs.push(`[${timestamp}] ${message}`);
  }

  serialize(): string {
    return JSON.stringify(this.logs);
  }

  deserialize(data: string): void {
    this.logs = JSON.parse(data);
  }
}

// Intersection dengan tipe primitif — berguna untuk branded types
type Email = string & { readonly __brand: unique symbol };
type UserId = number & { readonly __brand: unique symbol };

function createEmail(value: string): Email {
  if (!value.includes("@")) {
    throw new Error("Email tidak valid");
  }
  return value as Email;
}

function createUserId(value: number): UserId {
  return value as UserId;
}

const email = createEmail("budi@mail.com");
const userId = createUserId(1);

// ❌ Error: string tidak assignable ke Email
// const wrong: Email = "bukan email";

// ❌ Error: UserId tidak assignable ke Email
// const wrong2: Email = email;
// const wrong3: Email = userId;  // Error!

Intersection vs Extends

TypeScript — Intersection vs Extends
// Intersection type
type A = { nama: string };
type B = { umur: number };
type C = A & B;  // { nama: string; umur: number }

// Mirip dengan interface extends
interface D extends A, B {
  email: string;
}

// Perbedaan: intersection bisa untuk union types
type StringOrNumber = string | number;
type BooleanOrString = boolean | string;
type Combined = StringOrNumber & BooleanOrString;
// Hasil: string (irisan dari keduanya)

// Intersection untuk mixin pattern
type Constructor<T = {}> = new (...args: any[]) => T;

function Timestamped<TBase extends Constructor>(Base: TBase) {
  return class extends Base {
    createdAt = new Date();
    updatedAt = new Date();
  };
}

function Activatable<TBase extends Constructor>(Base: TBase) {
  return class extends Base {
    isActive = false;
    activate() { this.isActive = true; }
    deactivate() { this.isActive = false; }
  };
}

class BaseUser {
  constructor(public nama: string) {}
}

// Gabungkan mixins
const EnhancedUser = Timestamped(Activatable(BaseUser));
const user = new EnhancedUser("Budi");

console.log(user.nama);        // "Budi"
console.log(user.createdAt);   // Date
user.activate();
console.log(user.isActive);    // true

3. Discriminated Unions

Discriminated unions (tagged unions) adalah pola yang sangat powerful untuk memodelkan data yang bisa berada dalam beberapa bentuk berbeda.

TypeScript — Discriminated Unions
// Discriminated union — gunakan literal type sebagai "tag"
type Shape =
  | { kind: "circle"; radius: number }
  | { kind: "rectangle"; width: number; height: number }
  | { kind: "triangle"; base: number; height: number };

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

const lingkaran: Shape = { kind: "circle", radius: 10 };
const persegi: Shape = { kind: "rectangle", width: 5, height: 8 };

console.log(`Luas lingkaran: ${hitungLuas(lingkaran).toFixed(2)}`);
// Luas lingkaran: 314.16
console.log(`Luas persegi: ${hitungLuas(persegi)}`);
// Luas persegi: 40

// Discriminated union untuk API response
type ApiResponse<T> =
  | { status: "success"; data: T; pagination: { page: number; total: number } }
  | { status: "error"; error: { code: number; message: string } }
  | { status: "loading"; progress: number };

async function fetchData<T>(url: string): Promise<ApiResponse<T>> {
  try {
    const response = await fetch(url);
    if (!response.ok) {
      return {
        status: "error",
        error: { code: response.status, message: response.statusText }
      };
    }
    const data = await response.json();
    return {
      status: "success",
      data: data as T,
      pagination: { page: 1, total: 100 }
    };
  } catch (err) {
    return {
      status: "error",
      error: { code: 0, message: "Network error" }
    };
  }
}

// Discriminated union untuk state management
type TodoAction =
  | { type: "ADD"; payload: { text: string } }
  | { type: "TOGGLE"; payload: { id: number } }
  | { type: "DELETE"; payload: { id: number } }
  | { type: "EDIT"; payload: { id: number; text: string } }
  | { type: "CLEAR_COMPLETED" };

interface Todo {
  id: number;
  text: string;
  completed: boolean;
}

function todoReducer(state: Todo[], action: TodoAction): Todo[] {
  switch (action.type) {
    case "ADD":
      return [...state, {
        id: Date.now(),
        text: action.payload.text,
        completed: false
      }];
    case "TOGGLE":
      return state.map(todo =>
        todo.id === action.payload.id
          ? { ...todo, completed: !todo.completed }
          : todo
      );
    case "DELETE":
      return state.filter(todo => todo.id !== action.payload.id);
    case "EDIT":
      return state.map(todo =>
        todo.id === action.payload.id
          ? { ...todo, text: action.payload.text }
          : todo
      );
    case "CLEAR_COMPLETED":
      return state.filter(todo => !todo.completed);
  }
}

4. Conditional Types

Conditional types memungkinkan Anda memilih tipe berdasarkan kondisi, mirip dengan ternary operator tetapi untuk tipe.

TypeScript — Conditional Types
// Sintaks: T extends U ? X : Y
// Jika T assignable ke U, hasilnya X,搩戙 Y

type IsString<T> = T extends string ? true : false;

type Test1 = IsString<string>;    // true
type Test2 = IsString<number>;    // false
type Test3 = IsString<"hello">;   // true (literal extends string)

// Conditional type untuk tipe yang lebih fleksibel
type TypeName<T> =
  T extends string ? "string" :
  T extends number ? "number" :
  T extends boolean ? "boolean" :
  T extends Function ? "function" :
  T extends symbol ? "symbol" :
  T extends undefined ? "undefined" :
  T extends null ? "null" :
  "object";

type A = TypeName<string>;      // "string"
type B = TypeName<() => void>;  // "function"
type C = TypeName<string[]>;    // "object"

// Distributive conditional types
type ToArray<T> = T extends any ? T[] : never;

type StrArr = ToArray<string | number>;
// Hasil: string[] | number[] (bukan (string | number)[])

// Non-distributive — gunakan [T] untuk menghentikan distribusi
type ToArrayNonDist<T> = [T] extends [any] ? T[] : never;

type BothArr = ToArrayNonDist<string | number>;
// Hasil: (string | number)[]

// Exclude dari union — menggunakan conditional type
type MyExclude<T, U> = T extends U ? never : T;

type Result = MyExclude<"a" | "b" | "c", "a">
// "b" | "c"

// Extract — kebalikan dari Exclude
type MyExtract<T, U> = T extends U ? T : never;

type Result2 = MyExtract<"a" | "b" | "c", "a" | "b">
// "a" | "b"

Conditional Types dengan infer

TypeScript — Infer Keyword
// infer — menangkap tipe di posisi tertentu

// Ambil return type dari fungsi
type MyReturnType<T> = T extends (...args: any[]) => infer R ? R : never;

type FnReturn = MyReturnType<() => string>;     // string
type FnReturn2 = MyReturnType<(x: number) => boolean>;  // boolean

// Ambil parameter type pertama
type FirstParam<T> = T extends (first: infer F, ...rest: any[]) => any ? F : never;

type Param1 = FirstParam<(name: string, age: number) => void>;  // string

// Ambil tipe element dari array
type ElementType<T> = T extends (infer E)[] ? E : never;

type El = ElementType<string[]>;     // string
type El2 = ElementType<number[]>;    // number
type El3 = ElementType<(string | number)[]>;  // string | number

// Ambil tipe dari Promise
type UnwrapPromise<T> = T extends Promise<infer U> ? U : T;

type Unwrapped = UnwrapPromise<Promise<string>>;   // string
type Unwrapped2 = UnwrapPromise<number>;            // number (bukan Promise)

// Recursive conditional type — unwrap nested Promise
type DeepUnwrap<T> = T extends Promise<infer U> ? DeepUnwrap<U> : T;

type Deep = DeepUnwrap<Promise<Promise<Promise<string>>>>;
// string

// Ambil tipe dari object property
type PropType<T, K extends keyof T> = T extends { [key in K]: infer V } ? V : never;

interface User {
  nama: string;
  umur: number;
  aktif: boolean;
}

type NamaType = PropType<User, "nama">;   // string
type UmurType = PropType<User, "umur">;   // number

5. Mapped Types

Mapped types memungkinkan Anda membuat tipe baru dengan mentransformasi properti dari tipe yang sudah ada. Ini sangat powerful untuk membuat utility types.

TypeScript — Mapped Types
// Sintaks dasar: { [K in Keys]: ValueType }
type Keys = "nama" | "umur" | "email";

// Buat object type dari union
type UserMap = {
  [K in Keys]: string;
};
// { nama: string; umur: string; email: string }

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

interface User {
  nama: string;
  umur: number;
  email: string;
}

type UserGetters = Getters<User>;
// {
//   getNama: () => string;
//   getUmur: () => number;
//   getEmail: () => string;
// }

// Implementasi
const userGetters: UserGetters = {
  getNama: () => "Budi",
  getUmur: () => 25,
  getEmail: () => "budi@mail.com"
};

// Nullable mapped type
type Nullable<T> = {
  [K in keyof T]: T[K] | null;
};

type NullableUser = Nullable<User>;
// { nama: string | null; umur: number | null; email: string | null }

// Mutable mapped type (hapus readonly)
type Mutable<T> = {
  -readonly [K in keyof T]: T[K];
};

interface ReadonlyConfig {
  readonly apiUrl: string;
  readonly timeout: number;
}

type MutableConfig = Mutable<ReadonlyConfig>;
// { apiUrl: string; timeout: number } — readonly dihapus!

// Required mapped type (hapus optional)
type Concrete<T> = {
  [K in keyof T]-?: T[K];
};

interface PartialUser {
  nama?: string;
  umur?: number;
  email?: string;
}

type RequiredUser = Concrete<PartialUser>;
// { nama: string; umur: number; email: string } — semua wajib!

Advanced Mapped Types

TypeScript — Advanced Mapped
// Filter keys berdasarkan tipe value
type FilterByType<T, U> = {
  [K in keyof T as T[K] extends U ? K : never]: T[K];
};

interface MixedData {
  nama: string;
  umur: number;
  email: string;
  skor: number;
  aktif: boolean;
}

// Ambil hanya properti yang bertipe string
type StringProps = FilterByType<MixedData, string>;
// { nama: string; email: string }

// Ambil hanya properti yang bertipe number
type NumberProps = FilterByType<MixedData, number>;
// { umur: number; skor: number }

// Event handler pattern
type EventHandlers<T> = {
  [K in keyof T as `on${Capitalize<string & K>}Change`]: (
    newValue: T[K],
    oldValue: T[K]
  ) => void;
};

type UserEventHandlers = EventHandlers<User>;
// {
//   onNamaChange: (newValue: string, oldValue: string) => void;
//   onUmurChange: (newValue: number, oldValue: number) => void;
//   onEmailChange: (newValue: string, oldValue: string) => void;
// }

// Deep mapped type — recursive
type DeepReadonly<T> = {
  readonly [K in keyof T]: T[K] extends object
    ? T[K] extends Function
      ? T[K]
      : DeepReadonly<T[K]>
    : T[K];
};

interface Config {
  database: {
    host: string;
    port: number;
    credentials: {
      user: string;
      pass: string;
    };
  };
  cache: {
    ttl: number;
  };
}

type ReadonlyConfig = DeepReadonly<Config>;
// Semua nested properties jadi readonly!

6. Template Literal Types

Template literal types memungkinkan Anda membuat tipe string baru dari kombinasi string literal, bahkan dengan tipe lain.

TypeScript — Template Literals
// Template literal type dasar
type Greeting = `Hello, ${string}!`;

const greet1: Greeting = "Hello, World!";     // ✅
const greet2: Greeting = "Hello, TypeScript!"; // ✅
// const greet3: Greeting = "Hi, World!";      // ❌ Error

// Kombinasi dengan union
type Color = "red" | "green" | "blue";
type Size = "small" | "medium" | "large";

type ColorSize = `${Color}-${Size}`;
// "red-small" | "red-medium" | "red-large" |
// "green-small" | "green-medium" | "green-large" |
// "blue-small" | "blue-medium" | "blue-large"

const cs: ColorSize = "red-medium";  // ✅
// const cs2: ColorSize = "red-tiny"; // ❌ Error

// CSS property pattern
type CSSUnit = "px" | "em" | "rem" | "%" | "vh" | "vw";
type CSSValue = `${number}${CSSUnit}`;

const width: CSSValue = "100px";    // ✅
const fontSize: CSSValue = "1.5rem"; // ✅
// const wrong: CSSValue = "100";    // ❌ Error: missing unit

// Event naming pattern
type EventName = "click" | "focus" | "blur" | "change";
type EventHandler = `on${Capitalize<EventName>}`;
// "onClick" | "onFocus" | "onBlur" | "onChange"

// API route pattern
type HttpMethod = "GET" | "POST" | "PUT" | "DELETE";
type ApiVersion = "v1" | "v2";
type ApiRoute = `/${ApiVersion}/${string}`;

function createEndpoint(method: HttpMethod, route: ApiRoute): string {
  return `${method} ${route}`;
}

const endpoint = createEndpoint("GET", "/v1/users");
console.log(endpoint);  // "GET /v1/users"

Utility Types dengan Template Literals

TypeScript — Utility + Template
// Built-in string utility types
type Upper = Uppercase<"hello">;         // "HELLO"
type Lower = Lowercase<"HELLO">;         // "hello"
type Capital = Capitalize<"hello">;      // "Hello"
type Uncapital = Uncapitalize<"Hello">;  // "hello"

// Buat setter dari getter
type PropToSetter<T> = {
  [K in keyof T as `set${Capitalize<string & K>}`]: (value: T[K]) => void;
};

interface FormFields {
  username: string;
  password: string;
  rememberMe: boolean;
}

type FormSetters = PropToSetter<FormFields>;
// {
//   setUsername: (value: string) => void;
//   setPassword: (value: string) => void;
//   setRememberMe: (value: boolean) => void;
// }

// Route params extraction
type ExtractParams<T extends string> =
  T extends `${string}:${infer Param}/${infer Rest}`
    ? Param | ExtractParams<Rest>
    : T extends `${string}:${infer Param}`
      ? Param
      : never;

type Params = ExtractParams<"/users/:userId/posts/:postId">
// "userId" | "postId"

// Build path with params
type BuildPath<T extends string> =
  T extends `${string}:${infer Param}/${infer Rest}`
    ? Record<Param, string> & BuildPath<Rest>
    : T extends `${string}:${infer Param}`
      ? Record<Param, string>
      : {};

type UserPostParams = BuildPath<"/users/:userId/posts/:postId">
// { userId: string } & { postId: string }

7. Type Guards Lanjutan

Type guards adalah cara untuk mempersempit tipe di runtime. Dengan advanced types, type guards bisa sangat powerful.

TypeScript — Advanced Type Guards
// Custom type guard — fungsi yang mengembalikan "value is Type"
function isString(value: unknown): value is string {
  return typeof value === "string";
}

function isNumber(value: unknown): value is number {
  return typeof value === "number" && !isNaN(value);
}

function isArray<T>(value: unknown, itemGuard: (v: unknown) => v is T): value is T[] {
  return Array.isArray(value) && value.every(itemGuard);
}

// Discriminated union type guard
type Shape =
  | { kind: "circle"; radius: number }
  | { kind: "rectangle"; width: number; height: number };

function isCircle(shape: Shape): shape is Extract<Shape, { kind: "circle" }> {
  return shape.kind === "circle";
}

// Assertion function — throws if condition not met
function assertDefined<T>(
  value: T | null | undefined,
  message: string
): asserts value is T {
  if (value === null || value === undefined) {
    throw new Error(message);
  }
}

function processUser(user: { nama: string } | null): void {
  assertDefined(user, "User tidak boleh null!");
  // Setelah assert, TypeScript tahu user bukan null
  console.log(user.nama.toUpperCase());
}

// Assertion dengan typeof
function assertString(value: unknown, name: string): asserts value is string {
  if (typeof value !== "string") {
    throw new Error(`${name} harus string, dapat ${typeof value}`);
  }
}

function proses(data: unknown): void {
  assertString(data, "data");
  // TypeScript tahu data adalah string dari sini
  console.log(data.toUpperCase());
}

8. Praktik: Type-Safe State Machine

Mari kita gabungkan semua konsep advanced types untuk membuat type-safe state machine untuk proses order.

TypeScript — State Machine
// State machine untuk proses order
type OrderState =
  | { status: "draft"; items: string[] }
  | { status: "pending"; items: string[]; totalPrice: number }
  | { status: "confirmed"; items: string[]; totalPrice: number; orderId: string }
  | { status: "shipped"; items: string[]; totalPrice: number; orderId: string; trackingNumber: string }
  | { status: "delivered"; items: string[]; orderId: string; trackingNumber: string; deliveredAt: Date }
  | { status: "cancelled"; reason: string; cancelledAt: Date };

// Type-safe transition function
type Transition<From extends OrderState["status"], To extends OrderState["status"]> = {
  from: Extract<OrderState, { status: From }>;
  to: Extract<OrderState, { status: To }>;
};

// Valid transitions
type ValidTransitions =
  | Transition<"draft", "pending">
  | Transition<"pending", "confirmed">
  | Transition<"pending", "cancelled">
  | Transition<"confirmed", "shipped">
  | Transition<"confirmed", "cancelled">
  | Transition<"shipped", "delivered">

// Generic state machine class
class StateMachine<TStates extends { status: string }> {
  private currentState: TStates;
  private listeners: ((state: TStates) => void)[] = [];

  constructor(initialState: TStates) {
    this.currentState = initialState;
  }

  getState(): TStates {
    return this.currentState;
  }

  transition<TNew extends TStates>(newState: TNew): void {
    const oldState = this.currentState;
    this.currentState = newState;
    this.listeners.forEach(fn => fn(newState));
    console.log(`Transition: ${oldState.status} → ${newState.status}`);
  }

  onStateChange(listener: (state: TStates) => void): () => void {
    this.listeners.push(listener);
    return () => {
      this.listeners = this.listeners.filter(fn => fn !== listener);
    };
  }
}

// Type guard untuk setiap state
function isDraft(state: OrderState): state is Extract<OrderState, { status: "draft" }> {
  return state.status === "draft";
}

function isPending(state: OrderState): state is Extract<OrderState, { status: "pending" }> {
  return state.status === "pending";
}

// Usage
const orderMachine = new StateMachine<OrderState>({
  status: "draft",
  items: ["Laptop", "Mouse"]
});

// Type-safe transitions
orderMachine.transition({
  status: "pending",
  items: ["Laptop", "Mouse"],
  totalPrice: 15150000
});

orderMachine.transition({
  status: "confirmed",
  items: ["Laptop", "Mouse"],
  totalPrice: 15150000,
  orderId: "ORD-001"
});

console.log(orderMachine.getState().orderId);  // "ORD-001"
💡 Tips

Advanced types sangat powerful, tapi bisa membuat kode sulit dibaca jika berlebihan. Gunakan dengan bijak — tulis komentar yang jelas untuk menjelaskan maksud dari type yang kompleks. Ingat: kode yang bisa dibaca lebih baik daripada kode yang pintar tapi membingungkan.

9. Quiz: Uji Pemahamanmu!

Setelah membaca tutorial di atas, jawablah 5 pertanyaan berikut untuk menguji pemahamanmu tentang TypeScript Advanced Types:

Pertanyaan 1: Apa hasil dari string & (string | number)?

a) string | number
b) string
c) number
d) never

Pertanyaan 2: Apa fungsi dari infer di conditional types?

a) Menghapus tipe tertentu
b) Menangkap/mengambil tipe dari posisi tertentu
c) Membuat tipe baru dari literal
d) Menggabungkan dua tipe

Pertanyaan 3: Apa yang dilakukan mapped type { [K in keyof T]-?: T[K] }?

a) Membuat semua properti opsional
b) Membuat semua properti readonly
c) Membuat semua properti required (menghapus ?)
d) Menghapus semua properti

Pertanyaan 4: Apa keunggulan utama discriminated unions?

a) Membuat kode lebih pendek
b) Memungkinkan exhaustive checking dan narrowing otomatis
c) Menggantikan interface
d) Membuat kode berjalan lebih cepat

Pertanyaan 5: Apa hasil dari template literal type `${"a" | "b"}-${1 | 2}`?

a) "a-1"
b) "a-1" | "a-2" | "b-1" | "b-2"
c) string
d) "a-1" | "b-2"
🔍 Zoom
100%
🎹 Tema