Core TypeScript Syntax #
Every programming language has idioms and constructs that shape the way its users think. TypeScript is no exception — but because it’s built on top of JavaScript, there’s an extra layer you need to understand: TypeScript doesn’t replace JavaScript syntax, it extends it. You write JavaScript as usual, but with type annotations that give the compiler information to validate your code before it runs. This article covers the syntax constructs you’ll encounter most often when writing TypeScript — from basic type annotations, functions, interfaces, and classes, to advanced features like generics and decorators.
Type Annotations #
Type annotations are the most fundamental syntax that sets TypeScript apart from JavaScript. You add a type after a variable name, parameter, or return value using a colon (:). This is how you “talk” to the TypeScript compiler.
// Basic type annotations on variables
let selesai: boolean = false;
let usia: number = 30;
let nama: string = "Budi";
let daftar: number[] = [1, 2, 3, 4, 5];
// Alternative array syntax using Generics
let daftarLain: Array<string> = ["a", "b", "c"];
Type annotations are optional in many cases — TypeScript can infer types automatically through type inference. But there are situations where explicit annotations are safer and clearer:
// ANTI-PATTERN: Declaring a variable without an initial value and without a type annotation
// TypeScript infers the type 'any', losing the benefits of type safety
let nilai;
nilai = 42;
nilai = "teks"; // no error, even though this might be a bug
// CORRECT: Declare the type when the variable has no initial value yet
let nilaiBenar: number;
nilaiBenar = 42;
nilaiBenar = "teks"; // ✗ Error: Type 'string' is not assignable to type 'number'
TypeScript provides the following primitive types that are commonly used:
| Type | Description | Example Values |
|---|---|---|
boolean | Logical values | true, false |
number | All numbers (integer & float) | 42, 3.14, -7 |
string | Text | "halo", `template` |
null | Deliberate absence of a value | null |
undefined | Value not yet assigned | undefined |
any | Disables type checking | Any value |
unknown | Type-safe replacement for any | Any value (must be narrowed) |
never | A value that never occurs | Functions that always throw |
void | No return value | Return of a function without a value |
Typed Functions #
Functions in TypeScript can and should have type annotations on their parameters and return values. This ensures functions are used correctly — the compiler will reject calls with wrongly-typed arguments.
// Function with typed parameters and an explicit return type
function salam(nama: string): string {
return `Halo, ${nama}!`;
}
console.log(salam("Budi")); // ✓ Output: Halo, Budi!
// salam(123); // ✗ Error: Argument of type 'number' is not assignable to parameter of type 'string'
Optional and Default Parameters #
TypeScript supports optional parameters (with ?) and parameters with default values:
// Optional parameter using the '?' marker
function buatProfil(nama: string, usia?: number): string {
if (usia !== undefined) {
return `${nama}, ${usia} tahun`;
}
return nama;
}
// Parameter with a default value
function hitungDiskon(harga: number, diskon: number = 0.1): number {
return harga * (1 - diskon);
}
console.log(buatProfil("Budi")); // "Budi"
console.log(buatProfil("Budi", 25)); // "Budi, 25 tahun"
console.log(hitungDiskon(100000)); // 90000
console.log(hitungDiskon(100000, 0.2)); // 80000
Arrow Functions #
JavaScript arrow functions also support TypeScript type annotations:
// Arrow function with explicit types
const kali = (a: number, b: number): number => a * b;
// Types can be declared separately from the implementation
type FungsiMath = (x: number, y: number) => number;
const tambah: FungsiMath = (a, b) => a + b;
const kurang: FungsiMath = (a, b) => a - b;
Interface #
An interface defines a “contract” — the shape an object must satisfy. It’s the most expressive way to document data structures in TypeScript. Unlike classes, interfaces don’t generate any JavaScript code when compiled — they exist purely at the TypeScript level.
interface Pengguna {
id: number;
nama: string;
email: string;
dibuat: Date;
}
// An object must satisfy all required properties of the interface
const pengguna: Pengguna = {
id: 1,
nama: "Budi Santoso",
email: "[email protected]",
dibuat: new Date(),
};
Optional and Readonly Properties #
Interfaces support optional properties (with ?) and properties that can’t be changed after initialization (with readonly):
interface Produk {
readonly id: number; // can't be changed after creation
nama: string;
harga: number;
deskripsi?: string; // optional — may be absent
stok?: number; // optional
}
const produk: Produk = {
id: 101,
nama: "Laptop",
harga: 12000000,
};
// produk.id = 999; // ✗ Error: Cannot assign to 'id' because it is a read-only property
Interfaces for Functions #
Interfaces can also define the shape of a function:
interface FungsiValidator {
(nilai: string): boolean;
}
const validasiEmail: FungsiValidator = (email) => {
return email.includes("@");
};
const validasiNama: FungsiValidator = (nama) => {
return nama.length >= 2;
};
Extending Interfaces #
An interface can extend other interfaces, building a structured type hierarchy:
interface Entitas {
id: number;
dibuat: Date;
diperbarui: Date;
}
interface Pengguna extends Entitas {
nama: string;
email: string;
}
interface Admin extends Pengguna {
level: number;
izin: string[];
}
Class #
TypeScript classes are modern JavaScript classes with added type annotations, access modifiers, and other OOP features. Classes produce real JavaScript code when compiled — unlike interfaces.
class Hewan {
// Property with access modifier
private nama: string;
protected jenis: string;
public aktif: boolean;
constructor(nama: string, jenis: string) {
this.nama = nama;
this.jenis = jenis;
this.aktif = true;
}
// Public method
bunyikan(): void {
console.log(`${this.nama} membuat suara.`);
}
// Getter
get namaPanjang(): string {
return `${this.jenis}: ${this.nama}`;
}
}
const anjing = new Hewan("Rex", "Anjing");
anjing.bunyikan(); // "Rex membuat suara."
console.log(anjing.namaPanjang); // "Anjing: Rex"
// console.log(anjing.nama); // ✗ Error: Property 'nama' is private
Class Inheritance #
class Kucing extends Hewan {
private warnaBulu: string;
constructor(nama: string, warnaBulu: string) {
super(nama, "Kucing"); // Call the parent constructor
this.warnaBulu = warnaBulu;
}
// Override the parent method
bunyikan(): void {
console.log(`${this.jenis} ini mengucapkan: Meow!`);
}
info(): string {
return `Kucing berbulu ${this.warnaBulu}`;
}
}
const kucing = new Kucing("Mochi", "oranye");
kucing.bunyikan(); // "Kucing ini mengucapkan: Meow!"
Shorthand Constructor Parameters #
TypeScript has a concise syntax for declaring and initializing properties at the same time in the constructor:
// ANTI-PATTERN: Verbose — declaration and initialization are separate
class ProdukVerbose {
nama: string;
harga: number;
constructor(nama: string, harga: number) {
this.nama = nama;
this.harga = harga;
}
}
// CORRECT: Shorthand — declare directly in the constructor parameters
class Produk {
constructor(
public nama: string,
public harga: number,
private stok: number = 0
) {}
tersedia(): boolean {
return this.stok > 0;
}
}
Union Types and Type Aliases #
Union types allow a value to have more than one possible type. Type aliases give a name to complex types so they can be reused. Both are the most commonly used tools for expressing type flexibility in TypeScript.
// Union type — the value can be a string OR a number
let id: string | number;
id = "usr-123"; // ✓
id = 456; // ✓
// id = true; // ✗ Error: Type 'boolean' is not assignable to type 'string | number'
// Type alias for frequently used union types
type ID = string | number;
type Status = "aktif" | "nonaktif" | "pending";
type HasilOperasi = "sukses" | "gagal";
let statusPengguna: Status = "aktif";
// statusPengguna = "diblokir"; // ✗ Error: not part of the valid union
Narrowing Union Types #
When working with union types, you need to narrow before using methods specific to one type:
function prosesId(id: string | number): string {
// ANTI-PATTERN: Using a method directly without narrowing
// return id.toUpperCase(); // ✗ Error: 'toUpperCase' doesn't exist on 'number'
// CORRECT: Narrow first
if (typeof id === "string") {
return id.toUpperCase(); // TypeScript knows id is a string here
}
return id.toString(); // TypeScript knows id is a number here
}
Enum #
Enums define a set of named constants. They’re more expressive than using magic numbers or strings scattered across the codebase.
// Numeric enum — values start at 0 by default
enum Arah {
Atas, // 0
Bawah, // 1
Kiri, // 2
Kanan, // 3
}
let arahKarakter: Arah = Arah.Atas;
console.log(arahKarakter); // 0
// String enum — values are more descriptive and easier to debug
enum StatusPesanan {
Menunggu = "MENUNGGU",
Diproses = "DIPROSES",
Dikirim = "DIKIRIM",
Selesai = "SELESAI",
Dibatalkan = "DIBATALKAN",
}
function prosesStatusPesanan(status: StatusPesanan): void {
switch (status) {
case StatusPesanan.Menunggu:
console.log("Pesanan menunggu konfirmasi");
break;
case StatusPesanan.Dikirim:
console.log("Pesanan sedang dalam pengiriman");
break;
default:
console.log(`Status: ${status}`);
}
}
For string enums vs literal union types, the two can often replace each other. Literal union types (type Status = "aktif" | "nonaktif") are generally preferred because they’re lighter (no extra JS code generated) and easier to compose. Use enums when you need automatic numeric values or when the enum needs to be iterated.Generics #
Generics are a mechanism for creating components that work with many types without losing type information. This is a very powerful feature — the foundation of many abstractions in TypeScript libraries.
// Generic function — type T is a "placeholder" filled in at call time
function identitas<T>(nilai: T): T {
return nilai;
}
// TypeScript infers the type from the argument
console.log(identitas("halo")); // type: string
console.log(identitas(42)); // type: number
console.log(identitas(true)); // type: boolean
// Or give an explicit type
console.log(identitas<string>("eksplisit"));
Generics on Classes and Interfaces #
// Generic Stack — works for any type
class Stack<T> {
private items: T[] = [];
push(item: T): void {
this.items.push(item);
}
pop(): T | undefined {
return this.items.pop();
}
peek(): T | undefined {
return this.items[this.items.length - 1];
}
kosong(): boolean {
return this.items.length === 0;
}
}
const stackAngka = new Stack<number>();
stackAngka.push(1);
stackAngka.push(2);
stackAngka.push(3);
console.log(stackAngka.pop()); // 3
const stackTeks = new Stack<string>();
stackTeks.push("pertama");
stackTeks.push("kedua");
Generic Constraints #
You can restrict which types are allowed as a type parameter:
// T must have a 'panjang' property
interface PunyaPanjang {
panjang: number;
}
function cetakPanjang<T extends PunyaPanjang>(nilai: T): void {
console.log(`Panjang: ${nilai.panjang}`);
}
cetakPanjang("teks"); // ✓ strings have .length
cetakPanjang([1, 2, 3]); // ✓ arrays have .length
// cetakPanjang(42); // ✗ numbers don't have .length
Modules #
TypeScript uses the same ES module system as modern JavaScript — export to expose and import to consume. Every .ts file is its own module.
// src/utils/format.ts
export function formatMata(jumlah: number): string {
return new Intl.NumberFormat("id-ID", {
style: "currency",
currency: "IDR",
}).format(jumlah);
}
export function formatTanggal(tanggal: Date): string {
return new Intl.DateTimeFormat("id-ID").format(tanggal);
}
// Default export for the module's main export
export default function formatAngka(nilai: number): string {
return nilai.toLocaleString("id-ID");
}
// src/index.ts
// Named import
import { formatMata, formatTanggal } from "./utils/format";
// Default import
import formatAngka from "./utils/format";
// Import everything with a namespace alias
import * as Format from "./utils/format";
console.log(formatMata(150000)); // "Rp 150.000"
console.log(Format.formatMata(200000)); // "Rp 200.000"
Tuple #
A tuple is an array with a fixed number of elements and different types per position. Unlike a regular array where all elements share the same type.
// Regular array — all elements have the same type
const daftarAngka: number[] = [1, 2, 3];
// Tuple — types differ per position and are fixed
let pengguna: [string, number, boolean] = ["Budi", 25, true];
// Accessing tuple elements
console.log(pengguna[0]); // string: "Budi"
console.log(pengguna[1]); // number: 25
console.log(pengguna[2]); // boolean: true
// Destructuring a tuple
const [nama, usia, aktif] = pengguna;
console.log(`${nama} berusia ${usia} tahun`);
Tuples as Function Return Values #
Tuples are very useful for functions that need to return more than one value with different types:
function bagi(pembilang: number, penyebut: number): [number, string] {
if (penyebut === 0) {
return [0, "Error: tidak bisa dibagi nol"];
}
return [pembilang / penyebut, "sukses"];
}
const [hasil, pesan] = bagi(10, 3);
console.log(`Hasil: ${hasil}, Status: ${pesan}`);
// "Hasil: 3.3333333333333335, Status: sukses"
Optional Chaining and Nullish Coalescing #
These two modern operators handle a very common problem: accessing properties of values that might be null or undefined. Before these operators existed, null-handling code was very verbose.
interface Alamat {
kota: string;
provinsi: string;
kodePos?: string;
}
interface Profil {
nama: string;
alamat?: Alamat;
telepon?: string;
}
const profil: Profil = {
nama: "Budi Santoso",
// alamat not filled — undefined
};
// ANTI-PATTERN: Verbose manual checks
// if (profil && profil.alamat && profil.alamat.kota) {
// console.log(profil.alamat.kota);
// }
// CORRECT: Optional chaining (?.) — safe and concise
console.log(profil.alamat?.kota); // undefined (not an error)
console.log(profil.alamat?.kodePos); // undefined
// Nullish coalescing (??) — fallback value if null/undefined
const kota = profil.alamat?.kota ?? "Kota tidak diketahui";
const telepon = profil.telepon ?? "Tidak ada telepon";
console.log(kota); // "Kota tidak diketahui"
console.log(telepon); // "Tidak ada telepon"
The Difference Between ?? and ||
#
This is a frequent source of subtle bugs:
// '||' uses a falsy check — 0, "", false are all considered falsy
const stok = 0;
console.log(stok || 10); // 10 — WRONG! 0 is valid stock, not "no value"
// '??' only replaces null and undefined — far more precise
console.log(stok ?? 10); // 0 — CORRECT! 0 is valid stock
Decorator #
Decorators are an experimental feature (but already stable in TypeScript 5.x) that lets you add metadata or modify the behavior of classes, methods, or properties. Decorators are often used in frameworks like NestJS and Angular.
// Simple logging decorator
function logMetode(target: any, namaMetode: string, deskripsi: PropertyDescriptor) {
const metodeSumber = deskripsi.value;
deskripsi.value = function (...args: any[]) {
console.log(`Memanggil ${namaMetode} dengan argumen:`, args);
const hasil = metodeSumber.apply(this, args);
console.log(`${namaMetode} mengembalikan:`, hasil);
return hasil;
};
return deskripsi;
}
class KalkulatorHarga {
@logMetode
hitungTotal(harga: number, kuantitas: number): number {
return harga * kuantitas;
}
}
const kalkulator = new KalkulatorHarga();
kalkulator.hitungTotal(50000, 3);
// Log: Memanggil hitungTotal dengan argumen: [50000, 3]
// Log: hitungTotal mengembalikan: 150000
To use decorators, enable theexperimentalDecorators: trueoption intsconfig.json. In TypeScript 5.x, there are two “versions” of decorators — the old decorators (stage 2) and the new decorators (stage 3), which behave differently. Make sure you know which version the framework you’re using targets.
TypeScript Syntax Concept Map #
All the syntax constructs discussed in this article are interrelated. Here’s an overview of how they connect:
flowchart TD
A[TypeScript Syntax] --> B[Basic Types]
A --> C[Data Structures]
A --> D[Abstractions]
A --> E[Modern Operators]
B --> B1[boolean, number, string]
B --> B2[null, undefined, any, unknown]
B --> B3[Union Type]
B --> B4[Type Alias]
C --> C1[Array]
C --> C2[Tuple]
C --> C3[Enum]
D --> D1[Interface]
D --> D2[Class]
D --> D3[Generics]
D --> D4[Modules]
D --> D5[Decorator]
E --> E1[Optional Chaining ?.]
E --> E2[Nullish Coalescing ??]
D1 --> D2
D3 --> D1
D3 --> D2Summary #
- Type annotations — add a type after the name using
:, and enablestrict: trueso the compiler really validates all types, includingnullandundefined.- Union types (
string | number) — use when a value can have more than one type; always narrow before using type-specific methods.- Interface vs Type Alias — both define object shapes; interfaces are better for object hierarchies and can be
extended, type aliases are more flexible for unions and intersections.- Classes with access modifiers — use
private,protected,publicfor encapsulation; take advantage of shorthand constructor parameters to reduce boilerplate.- Generics — write a component once, use it for many types; use constraints (
extends) to restrict valid types.- Enums — suitable for named constants that need iterating or numeric values; for simple string literals, literal union types are preferred.
- Optional chaining (
?.) — safely access nested properties without verbose manual null checks.- Nullish coalescing (
??) — more precise than||for fallback values because it only replacesnullandundefined, not every falsy value like0or"".- Decorators — enable
experimentalDecoratorsin tsconfig; useful for logging, validation, and metadata in frameworks like NestJS.