Daftar Isi

Mengenal TypeScript: Type Assertions, Type Narrowing, & Function

This article has been translated into English version: Learn TypeScript: Type Assertions, Type Narrowing, & Function

TypeScript menawarkan berbagai cara untuk memanipulasi dan mempersempit tipe data. Artikel ini akan membahas tiga konsep penting: Type Assertions, Type Narrowing, dan Function Type Signatures.

Type Assertions

Type assertion adalah cara untuk memberi tahu TypeScript bahwa kita sebagai developer lebih tahu tipe data spesifik dari data yang kita miliki daripada yang bisa disimpulkan oleh TypeScript. Ini seperti berkata, “Percayalah padaku! Aku tahu apa yang aku lakukan 🤣”.

Ada dua sintaks untuk type assertion:

/** Sintaks dengan angle bracket (tidak direkomendasikan dalam JSX) */
let someValue: any = "Hello, TypeScript";
let strLength: number = (<string>someValue).length;

/** Sintaks dengan as (direkomendasikan) */
let someValue2: any = "Hello, TypeScript";
let strLength2: number = (someValue2 as string).length;

Type assertion berguna dalam beberapa situasi:

/** 1. Ketika bekerja dengan DOM */
const myInput = document.getElementById("myInput") as HTMLInputElement;
console.log(myInput.value); // Tanpa assertion, TypeScript tidak tahu bahwa ini adalah input

/** 2. Ketika bekerja dengan respons API */
interface User {
  id: number;
  name: string;
  email: string;
}

async function fetchUser() {
  const response = await fetch("https://api.example.com/user");
  const data = await response.json();

  return data as User; // Memastikan data memiliki struktur User
}

/** 3. Ketika bekerja dengan union type */
function processValue(value: string | number) {
  if (typeof value === "string") {
    return (value as string).toUpperCase();
  }

  return value.toFixed(2);
}

Penting untuk diingat bahwa type assertion di TypeScript bukan type casting seperti di bahasa pemrograman lain. Type assertion tidak mengubah struktur data pada runtime, hanya memberi tahu TypeScript tentang tipe yang seharusnya.

/** Ini akan berhasil di compile time tapi gagal di runtime */
const obj = { name: "John" };
const user = obj as User; // TypeScript tidak komplain
// console.log(user.email); // Runtime error: Cannot read property 'email' of undefined

Untuk assertion yang lebih aman, kita bisa menggunakan tipe unknown sebagai perantara:

/** Assertion bertingkat dengan unknown */
interface Product {
  id: string;
  price: number;
}

const data: any = { id: "prod-1", price: "100" }; // Perhatikan price adalah string

// Ini akan error di compile time:
// const product = data as Product; // Error: price harus number

// Dengan unknown sebagai perantara:
const product = data as unknown as Product; // TypeScript tidak komplain
console.log(product.price.toFixed(2)); // Runtime error: price.toFixed is not a function

Type Narrowing

Type narrowing adalah proses mempersempit tipe dari tipe yang lebih umum ke tipe yang lebih spesifik. Ini sangat berguna ketika bekerja dengan union types.

/** Type guard dengan typeof */
function formatValue(value: string | number): string {
  if (typeof value === "string") {
    // Di sini TypeScript tahu value adalah string
    return value.toUpperCase();
  }

  // Di sini TypeScript tahu value adalah number
  return value.toFixed(2);
}

console.log(formatValue("hello")); // "HELLO"
console.log(formatValue(42.123)); // "42.12"
/** Type guard dengan instanceof */
class Customer {
  name: string;
  email: string;

  constructor(name: string, email: string) {
    this.name = name;
    this.email = email;
  }

  sendEmail(): void {
    console.log(`Sending email to ${this.email}`);
  }
}

class Admin {
  name: string;
  permissions: string[];

  constructor(name: string, permissions: string[]) {
    this.name = name;
    this.permissions = permissions;
  }

  grantAccess(): void {
    console.log(`${this.name} has ${this.permissions.join(", ")} permissions`);
  }
}

function processUser(user: Customer | Admin): void {
  console.log(`Processing user: ${user.name}`);

  if (user instanceof Customer) {
    // Di sini TypeScript tahu user adalah Customer
    user.sendEmail();
  } else {
    // Di sini TypeScript tahu user adalah Admin
    user.grantAccess();
  }
}

const customer = new Customer("John", "john@example.com");
const admin = new Admin("Alice", ["read", "write", "delete"]);

processUser(customer); // "Processing user: John" "Sending email to john@example.com"
processUser(admin); // "Processing user: Alice" "Alice has read, write, delete permissions"

Kita juga bisa membuat fungsi type guard kustom dengan predicate type:

/** User-defined type guard */
interface Car {
  make: string;
  model: string;
  year: number;
}

interface Motorcycle {
  make: string;
  model: string;
  year: number;
  type: "sport" | "cruiser" | "touring";
}

/** Fungsi type guard dengan type predicate */
function isCar(vehicle: Car | Motorcycle): vehicle is Car {
  return (vehicle as Motorcycle).type === undefined;
}

function processVehicle(vehicle: Car | Motorcycle): void {
  console.log(`Vehicle: ${vehicle.make} ${vehicle.model} (${vehicle.year})`);

  if (isCar(vehicle)) {
    // Di sini TypeScript tahu vehicle adalah Car
    console.log("This is a car");
  } else {
    // Di sini TypeScript tahu vehicle adalah Motorcycle
    console.log(`This is a ${vehicle.type} motorcycle`);
  }
}

const car: Car = { make: "Toyota", model: "Corolla", year: 2020 };
const motorcycle: Motorcycle = {
  make: "Honda",
  model: "CBR",
  year: 2021,
  type: "sport",
};

processVehicle(car); // "Vehicle: Toyota Corolla (2020)" "This is a car"
processVehicle(motorcycle); // "Vehicle: Honda CBR (2021)" "This is a sport motorcycle"

Discriminated union adalah pola yang sangat kuat untuk type narrowing:

/** Discriminated union pattern */
interface SuccessResponse {
  status: "success";
  data: any;
}

interface ErrorResponse {
  status: "error";
  message: string;
}

type ApiResponse = SuccessResponse | ErrorResponse;

function handleResponse(response: ApiResponse): void {
  // Property 'status' adalah discriminant
  if (response.status === "success") {
    // Di sini TypeScript tahu response adalah SuccessResponse
    console.log(`Success: ${JSON.stringify(response.data)}`);
  } else {
    // Di sini TypeScript tahu response adalah ErrorResponse
    console.log(`Error: ${response.message}`);
  }
}

const successResponse: ApiResponse = {
  status: "success",
  data: { id: 1, name: "Product" },
};
const errorResponse: ApiResponse = { status: "error", message: "Not found" };

handleResponse(successResponse); // "Success: {"id":1,"name":"Product"}"
handleResponse(errorResponse); // "Error: Not found"

Function Type Signatures

TypeScript menawarkan cara yang kuat untuk mendefinisikan tipe fungsi, parameter, dan nilai kembalian.

/** Tipe fungsi dasar */
type GreetFunction = (name: string) => string;

const greet: GreetFunction = (name) => `Hello, ${name}!`;
console.log(greet("TypeScript")); // "Hello, TypeScript!"
/** Parameter opsional dan default */
function createUser(
  name: string,
  age?: number,
  isActive: boolean = true
): object {
  return {
    name,
    age: age ?? "Unknown",
    isActive,
  };
}

console.log(createUser("John")); // { name: "John", age: "Unknown", isActive: true }
console.log(createUser("Alice", 30)); // { name: "Alice", age: 30, isActive: true }
console.log(createUser("Bob", 25, false)); // { name: "Bob", age: 25, isActive: false }
/** Rest parameters */
function sum(...numbers: number[]): number {
  return numbers.reduce((total, num) => total + num, 0);
}

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

Function overloading memungkinkan kita mendefinisikan beberapa tipe untuk fungsi yang sama:

/** Function overloading */
// Overload signatures
function convert(value: string): number;
function convert(value: number): string;
function convert(value: boolean): string;

/** Implementation signature */
function convert(value: string | number | boolean): string | number {
  if (typeof value === "string") {
    return parseFloat(value) || 0;
  } else if (typeof value === "number") {
    return value.toString();
  } else {
    return value ? "true" : "false";
  }
}

console.log(convert("42")); // 42 (number)
console.log(convert(42)); // "42" (string)
console.log(convert(true)); // "true" (string)

Generic functions memungkinkan kita membuat fungsi yang bekerja dengan berbagai tipe:

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

const num = identity<number>(42); // 42 (type: number)
const str = identity<string>("hello"); // "hello" (type: string)
const bool = identity<boolean>(true); // true (type: boolean)

// TypeScript juga bisa menyimpulkan tipe secara otomatis
const auto = identity("TypeScript"); // "TypeScript" (type: string)

Penjelasan lebih mendalam tentang konsep Generics pada TypeScript bisa dibaca di sini: Mengenal TypeScript: Generics

Higher-order functions adalah fungsi yang menerima atau mengembalikan fungsi lain:

/** Higher-order function */
type Mapper<T, U> = (item: T) => U;

function mapArray<T, U>(array: T[], mapper: Mapper<T, U>): U[] {
  return array.map(mapper);
}

const numbers = [1, 2, 3, 4, 5];
const doubled = mapArray(numbers, (n) => n * 2);
const strings = mapArray(numbers, (n) => n.toString());

console.log(doubled); // [2, 4, 6, 8, 10]
console.log(strings); // ["1", "2", "3", "4", "5"]

TypeScript memungkinkan kita menentukan tipe untuk konteks this dalam fungsi:

/** Function dengan konteks this */
interface Counter {
  count: number;
  increment(): void;
  decrement(): void;
  getValue(): number;
}

function createCounter(): Counter {
  return {
    count: 0,
    increment(this: Counter): void {
      this.count++;
    },
    decrement(this: Counter): void {
      this.count--;
    },
    getValue(this: Counter): number {
      return this.count;
    },
  };
}

const counter = createCounter();
counter.increment();
counter.increment();
counter.decrement();
console.log(counter.getValue()); // 1

Contoh kode di atas menunjukkan bagaimana TypeScript memungkinkan kita mendefinisikan tipe untuk konteks this dalam fungsi. Dengan mendefinisikan this: Counter sebagai parameter pertama (yang sebenarnya tidak diteruskan saat pemanggilan), TypeScript akan memastikan bahwa fungsi-fungsi tersebut hanya dipanggil dalam konteks objek yang sesuai dengan interface Counter. Ini meningkatkan type safety karena TypeScript akan memberikan error jika kita mencoba menggunakan method tersebut dengan konteks this yang tidak sesuai, misalnya jika kita mencoba memanggil increment dengan bind ke objek yang tidak memiliki properti count.

Kesimpulan

Type assertions, type narrowing, dan function type signatures adalah fitur-fitur penting dalam TypeScript yang memungkinkan kita menulis kode yang lebih aman dan ekspresif. Type assertions memberikan kita cara untuk memberi tahu TypeScript tentang tipe yang kita yakini benar, type narrowing memungkinkan kita mempersempit tipe data untuk penanganan yang lebih spesifik, dan function type signatures memberikan cara yang kuat untuk mendefinisikan kontrak fungsi.

Dengan memahami dan memanfaatkan fitur-fitur ini, kita dapat menulis kode TypeScript yang lebih robust, dengan kesalahan yang lebih sedikit, dan lebih mudah dipelihara. Pendekatan functional programming menjadi lebih kuat ketika dikombinasikan dengan sistem tipe yang ekspresif seperti yang ditawarkan oleh TypeScript.

Konten Terkait