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
// 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
// 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.
// 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
// 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.
// 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.
// 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
// 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.
// 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
// 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.
// 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
// 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.
// 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.
// 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"
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: