TypeScript untuk Pemula: Dari JavaScript ke Type Safety dalam 30 Menit
Tutorial lengkap TypeScript untuk pemula. Pelajari type safety, interfaces, generics, dan best practices TypeScript dalam 30 menit dengan contoh praktis.
• 16 menit baca • Oleh Hilal Technologic

🚀 TypeScript untuk Pemula: Dari JavaScript ke Type Safety dalam 30 Menit
TypeScript telah menjadi standar industri untuk pengembangan JavaScript modern. Dengan adopsi yang mencapai 87% di kalangan developer profesional, TypeScript menawarkan type safety, better tooling, dan developer experience yang superior. Mari kita pelajari TypeScript dari dasar hingga mahir dalam 30 menit!
“TypeScript is JavaScript that scales.” - Microsoft TypeScript Team
🎯 Mengapa TypeScript?
Masalah dengan JavaScript
// JavaScript - Runtime errors yang sulit di-debug
function calculateTotal(price, tax) {
return price + tax; // Apa yang terjadi jika tax undefined?
}
calculateTotal(100); // NaN - Error baru ketahuan saat runtime
calculateTotal("100", "10"); // "10010" - String concatenation, bukan math
calculateTotal(100, { rate: 0.1 }); // "100[object Object]" - Unexpected behavior
// Typo yang tidak terdeteksi
const user = {
name: "John",
email: "[email protected]"
};
console.log(user.emial); // undefined - typo tidak terdeteksi
Solusi dengan TypeScript
// TypeScript - Errors tertangkap saat development
function calculateTotal(price: number, tax: number): number {
return price + tax;
}
calculateTotal(100); // ❌ Error: Expected 2 arguments, but got 1
calculateTotal("100", "10"); // ❌ Error: Argument of type 'string' is not assignable to parameter of type 'number'
calculateTotal(100, { rate: 0.1 }); // ❌ Error: Argument of type 'object' is not assignable to parameter of type 'number'
// Type-safe object access
interface User {
name: string;
email: string;
}
const user: User = {
name: "John",
email: "[email protected]"
};
console.log(user.emial); // ❌ Error: Property 'emial' does not exist on type 'User'. Did you mean 'email'?
Benefits TypeScript
- Early Error Detection - Catch bugs at compile time
- Better IDE Support - Autocomplete, refactoring, navigation
- Self-Documenting Code - Types serve as documentation
- Safer Refactoring - Confident code changes
- Team Collaboration - Clear contracts between functions
🛠️ Setup TypeScript
1. Installation
# Global installation
npm install -g typescript
# Project-specific installation
npm install -D typescript @types/node
# Initialize TypeScript config
npx tsc --init
# Install ts-node for development
npm install -D ts-node nodemon
2. TypeScript Configuration
// tsconfig.json
{
"compilerOptions": {
"target": "ES2020",
"module": "commonjs",
"lib": ["ES2020", "DOM"],
"outDir": "./dist",
"rootDir": "./src",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true,
"resolveJsonModule": true,
"declaration": true,
"declarationMap": true,
"sourceMap": true,
"removeComments": true,
"noImplicitAny": true,
"strictNullChecks": true,
"strictFunctionTypes": true,
"noImplicitReturns": true,
"noFallthroughCasesInSwitch": true,
"noUncheckedIndexedAccess": true
},
"include": ["src/**/*"],
"exclude": ["node_modules", "dist"]
}
3. Package.json Scripts
{
"scripts": {
"build": "tsc",
"dev": "ts-node src/index.ts",
"watch": "tsc --watch",
"start": "node dist/index.js",
"dev:watch": "nodemon --exec ts-node src/index.ts"
}
}
📚 TypeScript Fundamentals
1. Basic Types
// Primitive types
let name: string = "John";
let age: number = 30;
let isActive: boolean = true;
let data: null = null;
let value: undefined = undefined;
// Arrays
let numbers: number[] = [1, 2, 3, 4, 5];
let names: string[] = ["Alice", "Bob", "Charlie"];
let mixed: (string | number)[] = ["Alice", 25, "Bob", 30];
// Alternative array syntax
let scores: Array<number> = [95, 87, 92];
// Tuples - fixed length arrays with specific types
let person: [string, number] = ["John", 30];
let coordinate: [number, number, number] = [10, 20, 30];
// Enum
enum Color {
Red = "red",
Green = "green",
Blue = "blue"
}
enum Status {
Pending = 0,
Approved = 1,
Rejected = 2
}
let favoriteColor: Color = Color.Blue;
let currentStatus: Status = Status.Pending;
// Any (avoid when possible)
let anything: any = "hello";
anything = 42;
anything = true;
// Unknown (safer than any)
let userInput: unknown = "hello";
if (typeof userInput === "string") {
console.log(userInput.toUpperCase()); // Type guard required
}
// Void (for functions that don't return anything)
function logMessage(message: string): void {
console.log(message);
}
// Never (for functions that never return)
function throwError(message: string): never {
throw new Error(message);
}
2. Object Types dan Interfaces
// Object type annotation
let user: {
name: string;
age: number;
email?: string; // Optional property
} = {
name: "John",
age: 30
};
// Interface definition
interface User {
readonly id: number; // Read-only property
name: string;
age: number;
email?: string; // Optional property
hobbies: string[];
address: {
street: string;
city: string;
zipCode: string;
};
}
// Using interface
const newUser: User = {
id: 1,
name: "Alice",
age: 25,
email: "[email protected]",
hobbies: ["reading", "swimming"],
address: {
street: "123 Main St",
city: "New York",
zipCode: "10001"
}
};
// newUser.id = 2; // ❌ Error: Cannot assign to 'id' because it is a read-only property
// Interface inheritance
interface Employee extends User {
employeeId: string;
department: string;
salary: number;
}
const employee: Employee = {
id: 1,
name: "Bob",
age: 28,
hobbies: ["coding"],
address: {
street: "456 Oak Ave",
city: "San Francisco",
zipCode: "94102"
},
employeeId: "EMP001",
department: "Engineering",
salary: 75000
};
// Index signatures
interface StringDictionary {
[key: string]: string;
}
const translations: StringDictionary = {
hello: "hola",
goodbye: "adiós",
thank_you: "gracias"
};
3. Functions
// Function type annotations
function add(a: number, b: number): number {
return a + b;
}
// Arrow function
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 = 18): User {
return {
id: Math.random(),
name,
age,
hobbies: [],
address: {
street: "",
city: "",
zipCode: ""
}
};
}
// Rest parameters
function sum(...numbers: number[]): number {
return numbers.reduce((total, num) => total + num, 0);
}
// Function overloads
function format(value: string): string;
function format(value: number): string;
function format(value: boolean): string;
function format(value: string | number | boolean): string {
return String(value);
}
// Higher-order functions
function createValidator(minLength: number): (value: string) => boolean {
return (value: string) => value.length >= minLength;
}
const validatePassword = createValidator(8);
console.log(validatePassword("12345")); // false
console.log(validatePassword("12345678")); // true
// Function as interface
interface Calculator {
(a: number, b: number): number;
}
const subtract: Calculator = (a, b) => a - b;
const divide: Calculator = (a, b) => a / b;
🔧 Advanced TypeScript Features
1. Union Types dan Type Guards
// Union types
type Status = "loading" | "success" | "error";
type ID = string | number;
function processId(id: ID): string {
// Type guard with typeof
if (typeof id === "string") {
return id.toUpperCase(); // TypeScript knows id is string here
} else {
return id.toString(); // TypeScript knows id is number here
}
}
// Discriminated unions
interface LoadingState {
status: "loading";
}
interface SuccessState {
status: "success";
data: any;
}
interface ErrorState {
status: "error";
error: string;
}
type AppState = LoadingState | SuccessState | ErrorState;
function handleState(state: AppState) {
switch (state.status) {
case "loading":
console.log("Loading...");
break;
case "success":
console.log("Data:", state.data); // TypeScript knows state has data property
break;
case "error":
console.log("Error:", state.error); // TypeScript knows state has error property
break;
}
}
// Custom type guards
function isString(value: unknown): value is string {
return typeof value === "string";
}
function isUser(obj: any): obj is User {
return obj && typeof obj.name === "string" && typeof obj.age === "number";
}
function processUserData(data: unknown) {
if (isUser(data)) {
console.log(data.name); // TypeScript knows data is User
}
}
2. Generics
// Generic functions
function identity<T>(arg: T): T {
return arg;
}
const stringResult = identity<string>("hello"); // Type: string
const numberResult = identity<number>(42); // Type: number
const boolResult = identity(true); // Type inferred as boolean
// Generic interfaces
interface ApiResponse<T> {
data: T;
status: number;
message: string;
}
interface UserData {
id: number;
name: string;
email: string;
}
const userResponse: ApiResponse<UserData> = {
data: {
id: 1,
name: "John",
email: "[email protected]"
},
status: 200,
message: "Success"
};
const usersResponse: ApiResponse<UserData[]> = {
data: [
{ id: 1, name: "John", email: "[email protected]" },
{ id: 2, name: "Jane", email: "[email protected]" }
],
status: 200,
message: "Success"
};
// Generic classes
class DataStore<T> {
private items: T[] = [];
add(item: T): void {
this.items.push(item);
}
get(index: number): T | undefined {
return this.items[index];
}
getAll(): T[] {
return [...this.items];
}
find(predicate: (item: T) => boolean): T | undefined {
return this.items.find(predicate);
}
}
const userStore = new DataStore<User>();
const numberStore = new DataStore<number>();
userStore.add(newUser);
numberStore.add(42);
// Generic constraints
interface Lengthwise {
length: number;
}
function logLength<T extends Lengthwise>(arg: T): T {
console.log(arg.length); // Now we know arg has a length property
return arg;
}
logLength("hello"); // ✅ string has length
logLength([1, 2, 3]); // ✅ array has length
logLength({ length: 10, value: 3 }); // ✅ object with length property
// logLength(42); // ❌ Error: number doesn't have length property
// Keyof operator
function getProperty<T, K extends keyof T>(obj: T, key: K): T[K] {
return obj[key];
}
const user = { name: "John", age: 30, email: "[email protected]" };
const userName = getProperty(user, "name"); // Type: string
const userAge = getProperty(user, "age"); // Type: number
// const invalid = getProperty(user, "invalid"); // ❌ Error: Argument of type '"invalid"' is not assignable to parameter of type 'keyof User'
3. Utility Types
// Partial - makes all properties optional
interface UpdateUser {
name?: string;
age?: number;
email?: string;
}
// Using Partial utility type instead
type UpdateUserPartial = Partial<User>;
function updateUser(id: number, updates: Partial<User>): User {
// Implementation here
return {} as User;
}
// Required - makes all properties required
type RequiredUser = Required<User>; // All properties become required
// Pick - select specific properties
type UserSummary = Pick<User, "name" | "email">;
const summary: UserSummary = {
name: "John",
email: "[email protected]"
// age is not required
};
// Omit - exclude specific properties
type UserWithoutId = Omit<User, "id">;
const newUserData: UserWithoutId = {
name: "Jane",
age: 25,
hobbies: ["reading"],
address: {
street: "123 Main St",
city: "New York",
zipCode: "10001"
}
// id is not allowed
};
// Record - create object type with specific keys and values
type UserRoles = Record<string, string[]>;
const roles: UserRoles = {
admin: ["read", "write", "delete"],
user: ["read"],
guest: []
};
// Exclude and Extract
type T1 = Exclude<"a" | "b" | "c", "a">; // "b" | "c"
type T2 = Extract<"a" | "b" | "c", "a" | "f">; // "a"
// ReturnType - get return type of function
function createProduct() {
return {
id: 1,
name: "Product",
price: 100
};
}
type Product = ReturnType<typeof createProduct>; // { id: number; name: string; price: number; }
// Parameters - get parameters type of function
function processOrder(orderId: string, userId: number, items: string[]) {
// Implementation
}
type ProcessOrderParams = Parameters<typeof processOrder>; // [string, number, string[]]
🎨 Real-World Examples
1. API Client with TypeScript
// API response types
interface ApiError {
message: string;
code: number;
details?: any;
}
interface PaginatedResponse<T> {
data: T[];
pagination: {
page: number;
limit: number;
total: number;
totalPages: number;
};
}
// API client class
class ApiClient {
private baseUrl: string;
private defaultHeaders: Record<string, string>;
constructor(baseUrl: string, apiKey?: string) {
this.baseUrl = baseUrl;
this.defaultHeaders = {
'Content-Type': 'application/json',
...(apiKey && { 'Authorization': `Bearer ${apiKey}` })
};
}
private async request<T>(
endpoint: string,
options: RequestInit = {}
): Promise<T> {
const url = `${this.baseUrl}${endpoint}`;
const config: RequestInit = {
...options,
headers: {
...this.defaultHeaders,
...options.headers
}
};
try {
const response = await fetch(url, config);
if (!response.ok) {
const error: ApiError = await response.json();
throw new Error(`API Error: ${error.message}`);
}
return await response.json();
} catch (error) {
console.error('API request failed:', error);
throw error;
}
}
async get<T>(endpoint: string): Promise<T> {
return this.request<T>(endpoint, { method: 'GET' });
}
async post<T, U = any>(endpoint: string, data: U): Promise<T> {
return this.request<T>(endpoint, {
method: 'POST',
body: JSON.stringify(data)
});
}
async put<T, U = any>(endpoint: string, data: U): Promise<T> {
return this.request<T>(endpoint, {
method: 'PUT',
body: JSON.stringify(data)
});
}
async delete<T>(endpoint: string): Promise<T> {
return this.request<T>(endpoint, { method: 'DELETE' });
}
}
// Usage
const api = new ApiClient('https://api.example.com', 'your-api-key');
// Type-safe API calls
interface Product {
id: number;
name: string;
price: number;
category: string;
}
async function fetchProducts(): Promise<Product[]> {
const response = await api.get<PaginatedResponse<Product>>('/products');
return response.data;
}
async function createProduct(productData: Omit<Product, 'id'>): Promise<Product> {
return api.post<Product>('/products', productData);
}
2. React Component with TypeScript
import React, { useState, useEffect } from 'react';
// Props interface
interface UserCardProps {
user: User;
onEdit?: (user: User) => void;
onDelete?: (userId: number) => void;
className?: string;
showActions?: boolean;
}
// Component with TypeScript
const UserCard: React.FC<UserCardProps> = ({
user,
onEdit,
onDelete,
className = '',
showActions = true
}) => {
const [isEditing, setIsEditing] = useState<boolean>(false);
const [editedUser, setEditedUser] = useState<User>(user);
useEffect(() => {
setEditedUser(user);
}, [user]);
const handleSave = () => {
if (onEdit) {
onEdit(editedUser);
}
setIsEditing(false);
};
const handleCancel = () => {
setEditedUser(user);
setIsEditing(false);
};
const handleInputChange = (field: keyof User, value: string | number) => {
setEditedUser(prev => ({
...prev,
[field]: value
}));
};
return (
<div className={`user-card ${className}`}>
{isEditing ? (
<div className="edit-form">
<input
type="text"
value={editedUser.name}
onChange={(e) => handleInputChange('name', e.target.value)}
placeholder="Name"
/>
<input
type="number"
value={editedUser.age}
onChange={(e) => handleInputChange('age', parseInt(e.target.value))}
placeholder="Age"
/>
<input
type="email"
value={editedUser.email || ''}
onChange={(e) => handleInputChange('email', e.target.value)}
placeholder="Email"
/>
<div className="actions">
<button onClick={handleSave}>Save</button>
<button onClick={handleCancel}>Cancel</button>
</div>
</div>
) : (
<div className="user-info">
<h3>{user.name}</h3>
<p>Age: {user.age}</p>
{user.email && <p>Email: {user.email}</p>}
{showActions && (
<div className="actions">
<button onClick={() => setIsEditing(true)}>Edit</button>
{onDelete && (
<button onClick={() => onDelete(user.id)}>Delete</button>
)}
</div>
)}
</div>
)}
</div>
);
};
// Custom hooks with TypeScript
function useLocalStorage<T>(key: string, initialValue: T) {
const [storedValue, setStoredValue] = useState<T>(() => {
try {
const item = window.localStorage.getItem(key);
return item ? JSON.parse(item) : initialValue;
} catch (error) {
console.error(`Error reading localStorage key "${key}":`, error);
return initialValue;
}
});
const setValue = (value: T | ((val: T) => T)) => {
try {
const valueToStore = value instanceof Function ? value(storedValue) : value;
setStoredValue(valueToStore);
window.localStorage.setItem(key, JSON.stringify(valueToStore));
} catch (error) {
console.error(`Error setting localStorage key "${key}":`, error);
}
};
return [storedValue, setValue] as const;
}
// Usage
function App() {
const [users, setUsers] = useLocalStorage<User[]>('users', []);
const handleEditUser = (updatedUser: User) => {
setUsers(prev =>
prev.map(user => user.id === updatedUser.id ? updatedUser : user)
);
};
const handleDeleteUser = (userId: number) => {
setUsers(prev => prev.filter(user => user.id !== userId));
};
return (
<div className="app">
{users.map(user => (
<UserCard
key={user.id}
user={user}
onEdit={handleEditUser}
onDelete={handleDeleteUser}
/>
))}
</div>
);
}
🔍 Best Practices
1. Type Definitions
// ✅ Good: Descriptive and specific types
interface CreateUserRequest {
name: string;
email: string;
age: number;
department: string;
}
interface CreateUserResponse {
id: number;
user: User;
createdAt: string;
}
// ❌ Bad: Generic and unclear types
interface Data {
stuff: any;
things: any[];
}
// ✅ Good: Use enums for constants
enum UserRole {
ADMIN = 'admin',
USER = 'user',
GUEST = 'guest'
}
// ❌ Bad: Magic strings
const role = 'admin'; // What are the valid values?
// ✅ Good: Use readonly for immutable data
interface ReadonlyConfig {
readonly apiUrl: string;
readonly timeout: number;
readonly retries: number;
}
// ✅ Good: Use strict null checks
function processUser(user: User | null): string {
if (user === null) {
return 'No user provided';
}
return `Processing ${user.name}`;
}
// ❌ Bad: Assuming non-null
function processUserBad(user: User): string {
return `Processing ${user.name}`; // Could crash if user is null
}
2. Error Handling
// Custom error types
class ValidationError extends Error {
constructor(
message: string,
public field: string,
public value: any
) {
super(message);
this.name = 'ValidationError';
}
}
class NotFoundError extends Error {
constructor(resource: string, id: string | number) {
super(`${resource} with id ${id} not found`);
this.name = 'NotFoundError';
}
}
// Result type for error handling
type Result<T, E = Error> =
| { success: true; data: T }
| { success: false; error: E };
async function fetchUser(id: number): Promise<Result<User, NotFoundError | ValidationError>> {
try {
if (id <= 0) {
return {
success: false,
error: new ValidationError('Invalid user ID', 'id', id)
};
}
const response = await fetch(`/api/users/${id}`);
if (response.status === 404) {
return {
success: false,
error: new NotFoundError('User', id)
};
}
const user = await response.json();
return { success: true, data: user };
} catch (error) {
return {
success: false,
error: error instanceof Error ? error : new Error('Unknown error')
};
}
}
// Usage with proper error handling
async function handleUserFetch(id: number) {
const result = await fetchUser(id);
if (result.success) {
console.log('User:', result.data.name);
} else {
if (result.error instanceof ValidationError) {
console.error('Validation error:', result.error.message);
} else if (result.error instanceof NotFoundError) {
console.error('User not found:', result.error.message);
} else {
console.error('Unexpected error:', result.error.message);
}
}
}
3. Performance Optimizations
// Lazy loading with dynamic imports
async function loadHeavyModule() {
const { HeavyComponent } = await import('./HeavyComponent');
return HeavyComponent;
}
// Type-only imports (don't include in bundle)
import type { User } from './types/User';
import type { ApiResponse } from './types/Api';
// Actual imports
import { validateUser } from './utils/validation';
// Const assertions for better type inference
const themes = ['light', 'dark', 'auto'] as const;
type Theme = typeof themes[number]; // 'light' | 'dark' | 'auto'
// Template literal types
type EventName = `on${Capitalize<string>}`;
type ButtonEvent = `button${Capitalize<string>}`;
// Mapped types for transformations
type Optional<T> = {
[K in keyof T]?: T[K];
};
type Readonly<T> = {
readonly [K in keyof T]: T[K];
};
🚀 Migration Strategy
From JavaScript to TypeScript
// Step 1: Rename .js to .ts files
// Step 2: Add basic type annotations
// Before (JavaScript)
function calculateDiscount(price, discountPercent) {
return price * (discountPercent / 100);
}
// After (TypeScript)
function calculateDiscount(price: number, discountPercent: number): number {
return price * (discountPercent / 100);
}
// Step 3: Add interfaces for objects
interface Product {
id: number;
name: string;
price: number;
category: string;
}
// Step 4: Use strict mode gradually
// Start with "strict": false, then enable one by one:
// "noImplicitAny": true
// "strictNullChecks": true
// "strictFunctionTypes": true
// etc.
// Step 5: Add type definitions for external libraries
// npm install @types/lodash @types/express @types/node
🎯 Common Pitfalls dan Solutions
1. Any Type Abuse
// ❌ Bad: Using any everywhere
function processData(data: any): any {
return data.someProperty.anotherProperty;
}
// ✅ Good: Use specific types
interface ApiData {
user: {
profile: {
name: string;
email: string;
};
};
}
function processData(data: ApiData): string {
return data.user.profile.name;
}
// ✅ Good: Use unknown for truly unknown data
function processUnknownData(data: unknown): string {
if (isApiData(data)) {
return data.user.profile.name;
}
throw new Error('Invalid data format');
}
function isApiData(data: unknown): data is ApiData {
return (
typeof data === 'object' &&
data !== null &&
'user' in data &&
typeof (data as any).user.profile.name === 'string'
);
}
2. Type Assertions vs Type Guards
// ❌ Bad: Type assertion without validation
function processUser(data: unknown) {
const user = data as User; // Dangerous!
return user.name.toUpperCase();
}
// ✅ Good: Type guard with validation
function processUser(data: unknown) {
if (isUser(data)) {
return data.name.toUpperCase(); // Safe!
}
throw new Error('Invalid user data');
}
function isUser(data: unknown): data is User {
return (
typeof data === 'object' &&
data !== null &&
'name' in data &&
'age' in data &&
typeof (data as any).name === 'string' &&
typeof (data as any).age === 'number'
);
}
🔗 Resources dan Next Steps
Essential Tools
# TypeScript compiler
npm install -g typescript
# Type checking in CI/CD
npm install -D tsc-files
# ESLint with TypeScript
npm install -D @typescript-eslint/parser @typescript-eslint/eslint-plugin
# Prettier for formatting
npm install -D prettier
# Type definitions
npm install -D