TypeScript Cho Người Mới Bắt Đầu — Nâng Cấp JavaScript Lên Tầm Cao Mới
Học TypeScript từ con số 0 cho developer JavaScript. Tìm hiểu type system, interfaces, generics và cách tích hợp vào dự án hiện có. Viết code an toàn hơn, ít lỗi hơn.
Bạn có bao giờ tự hỏi tại sao cùng một function nhận input trông vô hại nhưng lại trả về kết quả kỳ lạ? Hay lý do vì sao refactoring một đoạn code cũ khiến cả dự án “nổ” hàng loạt lỗi không ngờ tới? Đó chính xác là những vấn đề mà TypeScript giải quyết — bằng cách thêm hệ thống kiểu tĩnh vào JavaScript.

Nếu bạn đã quen thuộc với JavaScript nhưng chưa từng chạm vào TypeScript, bài viết này sẽ dẫn bạn từng bước từ cơ bản đến nâng cao. Không cần kiến thức nền về lập trình hướng đối tượng hay type theory — chỉ cần bạn biết viết JavaScript là đủ.
Tại Sao Cần TypeScript?
Hãy xem xét ví dụ đơn giản này:
// JavaScript thuần — không có cảnh báo lỗi
function calculateTotal(price, quantity) {
return price * quantity;
}
calculateTotal(100, 3); // ✅ 300
calculateTotal(100, "3"); // ❌ 1003 (string concatenation!)
calculateTotal(100, undefined); // ❌ NaN
Trong JavaScript, "3" (string) và 3 (number) đều “khớp” với tham số quantity. Kết quả? Hàm trả về "1003" thay vì 300 — một bug khó phát hiện nếu không có test case đầy đủ.
Với TypeScript, lỗi này bị chặn ngay từ lúc viết code:
// TypeScript — lỗi ngay tại dòng gọi hàm
function calculateTotal(price: number, quantity: number): number {
return price * quantity;
}
calculateTotal(100, 3); // ✅ OK
calculateTotal(100, "3"); // ❌ Lỗi compile-time!
calculateTotal(100, undefined); // ❌ Lỗi compile-time!
Lợi Ích Cốt Lõi Của TypeScript
| Lợi ích | Mô tả |
|---|---|
| Phát hiện lỗi sớm | Bắt lỗi trước khi chạy code, không phải khi người dùng gặp phải |
| IntelliSense mạnh mẽ | Autocomplete thông minh, gợi ý parameter, document inline |
| Tài liệu sống | Type annotations tự động mô tả function/method nhận gì, trả gì |
| Refactoring an toàn | Đổi tên variable/function khắp dự án mà không sợ quên chỗ nào |
| Hợp tác nhóm | Developer mới đọc type signature là hiểu interface của module |
Cài Đặt Và Cấu Hình
Bước 1: Cài đặt TypeScript
npm install -D typescript
Bước 2: Tạo file cấu hình
npx tsc --init
File tsconfig.json được tạo ra với nhiều tùy chọn. Đây là cấu hình tối thiểu cho dự án web:
{
"compilerOptions": {
"target": "ES2020",
"module": "ESNext",
"moduleResolution": "bundler",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true,
"outDir": "./dist",
"rootDir": "./src"
},
"include": ["src/**/*"],
"exclude": ["node_modules", "dist"]
}
Bước 3: Biên dịch
# Build một lần
npx tsc
# Watch mode — tự động rebuild khi file thay đổi
npx tsc --watch
Hệ Thống Kiểu Cơ Bản
Các Kiểu Nguyên Thủy (Primitive Types)
// String
const name: string = "Devs2";
const greeting: string = `Chào ${name}!`;
// Number (bao gồm cả float và integer)
const age: number = 25;
const pi: number = 3.14159;
// Boolean
const isActive: boolean = true;
const isDeleted: boolean = false;
// Undefined & Null
let nothing: undefined = undefined;
let empty: null = null;
// Symbol (ES6+)
const id: symbol = Symbol("userId");
// BigInt (cho số cực lớn)
const bigNumber: bigint = 9007199254740991n;
Mẹo: Trong hầu hết trường hợp, bạn không cần ghi rõ type annotation. TypeScript có type inference — nó tự suy luận type từ giá trị gán:
// Cả hai cách đều đúng, nhưng cách thứ hai gọn hơn
const name: string = "Devs2";
const name = "Devs2"; // TypeScript tự hiểu là string
Kiểu Mảng (Arrays)
// Cách 1: Dùng cú pháp ElementType[]
const numbers: number[] = [1, 2, 3, 4, 5];
const names: string[] = ["An", "Bình", "Chi"];
// Cách 2: Dùng Generic Array<Type>
const scores: Array<number> = [90, 85, 78, 92];
// Mảng các object
interface User {
id: number;
name: string;
email: string;
}
const users: User[] = [
{ id: 1, name: "An", email: "an@example.com" },
{ id: 2, name: "Bình", email: "binh@example.com" }
];
Tuple — Mảng Có Kích Thước Cố Định
Tuple cho phép bạn định nghĩa mảng với số lượng phần tử cố định, mỗi phần tử có thể có type khác nhau:
// [string, number] — luôn có đúng 2 phần tử: string rồi number
const userRecord: [string, number] = ["Nguyễn Văn An", 25];
// Truy cập an toàn
const userName = userRecord[0]; // string
const userAge = userRecord[1]; // number
// ❌ Lỗi — sai thứ tự type
const badRecord: [string, number] = [25, "An"];
// ❌ Lỗi — thiếu phần tử
const incomplete: [string, number] = ["An"];
Tuple hữu ích khi làm việc với API trả về cặp giá trị, hoặc khi cần group dữ liệu nhỏ:
// Xử lý response từ API
async function fetchUser(id: number): Promise<[User, ResponseMetadata]> {
const response = await fetch(`/api/users/${id}`);
const data = await response.json();
return [data.user, { status: response.status, timestamp: Date.now() }];
}
const [user, meta] = await fetchUser(42);
console.log(user.name, meta.timestamp);
Enum — Tập Hợp Các Hằng Có Tên
Enum giúp bạn định nghĩa tập hợp các giá trị có tên, thay vì dùng magic strings/numbers:
// Numeric enum (mặc định bắt đầu từ 0)
enum Role {
Admin, // 0
Editor, // 1
Viewer // 2
}
// String enum (thường dùng trong thực tế)
enum Status {
Pending = "pending",
Active = "active",
Inactive = "inactive",
Archived = "archived"
}
// Sử dụng
const currentUserRole: Role = Role.Admin;
const postStatus: Status = Status.Active;
// Kiểm tra
if (currentUserRole === Role.Admin) {
console.log("Người dùng có quyền admin");
}
Any, Unknown Và Never
// any — TẮT kiểm tra kiểu (dùng khi thực sự cần)
let data: any = "hello";
data = 42; // OK
data = []; // OK
data.myMethod(); // ❌ Không có autocomplete, dễ gây lỗi runtime
// unknown — An toàn hơn any (phải kiểm tra trước khi dùng)
let safeData: unknown = "hello";
// safeData.myMethod(); // ❌ Lỗi compile-time
if (typeof safeData === "string") {
console.log(safeData.toUpperCase()); // ✅ OK — narrowed type
}
// never — Giá trị không bao giờ tồn tại (dùng cho error handling)
function throwError(message: string): never {
throw new Error(message);
}
function assertNever(value: never): never {
throw new Error(`Unexpected value: ${value}`);
}
Quy tắc vàng: Tránh dùng
anycàng ít càng tốt. Ưu tiênunknownkhi không chắc type, và dùng type narrowing để xử lý an toàn.
Interfaces Và Types
Đây là hai tính năng quan trọng nhất của TypeScript. Chúng giúp bạn định nghĩa cấu trúc của object.
Interface — Định Nghĩa Cấu Trúc Object
interface User {
id: number;
name: string;
email: string;
age?: number; // Optional property (?)
readonly createdAt: Date; // Readonly property
}
// Tuân thủ interface
const user: User = {
id: 1,
name: "An",
email: "an@example.com",
createdAt: new Date()
};
// ❌ Thiếu property bắt buộc
const badUser: User = {
id: 2,
name: "Bình"
// ❌ Lỗi: Missing property 'email' and 'createdAt'
};
// ❌ Property không tồn tại
const extraUser: User = {
id: 3,
name: "Chi",
email: "chi@example.com",
createdAt: new Date(),
phone: "0123456789" // ❌ Lỗi: Extra property 'phone'
};
Index Signature — Property Động
Khi bạn không biết trước tên các property:
interface Config {
[key: string]: string | number | boolean;
}
const settings: Config = {
theme: "dark",
fontSize: 16,
notifications: true,
language: "vi"
};
Type Alias — Linh Hoạt Hơn Interface
// Type alias có thể định nghĩa union, intersection, tuple...
type UserID = number | string;
type Theme = "light" | "dark" | "system";
// Intersection — kết hợp nhiều type
type AdminUser = User & { permissions: string[] };
// Union — một trong nhiều type
type Result<T> = T | Error;
// Conditional type
type IsString<T> = T extends string ? true : false;
type A = IsString<"hello">; // true
type B = IsString<42>; // false
Khi Nào Dùng Interface, Khi Nào Dùng Type?
Dùng interface | Dùng type |
|---|---|
| Định nghĩa object shape | Union types (A | B) |
Cần extends / implements | Intersection types (A & B) |
| Có thể merge (declaration merging) | Tuple types |
| Cho public API libraries | Conditional types |
Trong thực tế, interface thường được ưu tiên cho object shapes còn type dùng cho các trường hợp đặc biệt.
Functions Với Type Annotations
Gán Type Cho Tham Số Và Return Value
// Cú pháp: functionName(params: Type): ReturnType { ... }
function greet(name: string): string {
return `Xin chào, ${name}!`;
}
function add(a: number, b: number): number {
return a + b;
}
// Arrow function
const multiply = (a: number, b: number): number => a * b;
Optional Parameters Và Default Values
// ? — optional parameter
function createUser(name: string, role?: string): string {
return role
? `Tạo user ${name} với vai trò ${role}`
: `Tạo user ${name} (mặc định)`;
}
createUser("An"); // "Tạo user An (mặc định)"
createUser("Bình", "admin"); // "Tạo user Bình với vai trò admin"
// Default value
function connect(host: string = "localhost", port: number = 3000): string {
return `Kết nối đến ${host}:${port}`;
}
connect(); // "Kết nối đến localhost:3000"
connect("api.example.com"); // "Kết nối đến api.example.com:3000"
Rest Parameters Với Type
// ...args: string[] — thu thập tất cả tham số dư thừa vào mảng
function joinStrings(separator: string, ...parts: string[]): string {
return parts.join(separator);
}
joinStrings(", ", "HTML", "CSS", "JavaScript");
// "HTML, CSS, JavaScript"
joinStrings(" - ", "React", "Vue", "Angular", "Svelte");
// "React - Vue - Angular - Svelte"
Function Overloading — Nhiều Signature Cho Cùng Một Function
// Khai báo các overload signatures
function formatDate(date: Date): string;
function formatDate(year: number, month: number, day: number): string;
function formatDate(input: Date | number, month?: number, day?: number): string {
if (input instanceof Date) {
return input.toLocaleDateString('vi-VN');
}
// year, month, day
const date = new Date(input, month!, day!);
return date.toLocaleDateString('vi-VN');
}
// Sử dụng
formatDate(new Date()); // "04/08/2026"
formatDate(2026, 7, 4); // "04/08/2026"
Generics — Code Tái Sử Dụng Với Mọi Type
Generics cho phép bạn viết function/class linh hoạt với type mà vẫn giữ được kiểm tra kiểu.
Generic Function Cơ Bản
// <T> — T là một placeholder cho bất kỳ type nào
function identity<T>(arg: T): T {
return arg;
}
identity<string>("Hello"); // T = string, trả về "Hello"
identity<number>(42); // T = number, trả về 42
identity<boolean>(true); // T = boolean, trả về true
// Type inference — không cần ghi rõ <string>
identity("Hello"); // T tự động là string
Generic Với Object Shape
// ApiResponse<T> — response API chứa data của type T
interface ApiResponse<T> {
success: boolean;
data: T;
message: string;
}
// Dùng generic để tạo response typed
function createResponse<T>(data: T, message: string = "OK"): ApiResponse<T> {
return {
success: true,
data,
message
};
}
// Tự động infer type từ data
const userResponse = createResponse({ id: 1, name: "An" });
// ApiResponse<{ id: number; name: string }>
const listResponse = createResponse([1, 2, 3]);
// ApiResponse<number[]>
Generic Constraint — Giới Hạn Type
// <T extends { length: number }> — T phải có property 'length'
function getLength<T extends { length: number }>(arg: T): number {
return arg.length;
}
getLength("hello"); // ✅ string có .length → 5
getLength([1, 2, 3]); // ✅ array có .length → 3
// getLength(42); // ❌ number không có .length
Generic Class
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];
}
get size(): number {
return this.items.length;
}
}
// Stack số
const numStack = new Stack<number>();
numStack.push(1);
numStack.push(2);
numStack.push(3);
console.log(numStack.peek()); // 3
// Stack string — hoàn toàn type-safe
const strStack = new Stack<string>();
strStack.push("Hello");
// strStack.push(42); // ❌ Lỗi compile-time!
Utility Types — Built-In Generics Của TypeScript
TypeScript cung cấp sẵn nhiều utility types hữu dụng:
interface User {
id: number;
name: string;
email: string;
password: string;
}
// Partial<T> — tất cả property đều optional
type UpdateUser = Partial<User>;
// { id?: number; name?: string; email?: string; password?: string }
// Required<T> — tất cả property đều bắt buộc
type CreateUser = Required<User>;
// Pick<T, K> — chọn ra một số property
type PublicUser = Pick<User, 'id' | 'name' | 'email'>;
// { id: number; name: string; email: string }
// Omit<T, K> — loại bỏ một số property
type SafeUser = Omit<User, 'password'>;
// { id: number; name: string; email: string }
// Record<K, V> — tạo object type với key và value type
type UserRoleMap = Record<string, string[]>;
const roles: UserRoleMap = {
admin: ['read', 'write', 'delete'],
editor: ['read', 'write'],
viewer: ['read']
};
// Readonly<T> — tất cả property đều readonly
type ReadonlyUser = Readonly<User>;
// const user: ReadonlyUser = {...};
// user.name = "New Name"; // ❌ Cannot assign to 'name' because it is a read-only property
Type Narrowing — Thu Hẹp Type
TypeScript có thể “thu hẹp” type dựa trên điều kiện runtime:
Type Guard Với typeof
function padLeft(value: string, padding: string | number): string {
if (typeof padding === "number") {
// Trong khối này, padding là number
return " ".repeat(padding) + value;
}
// Ở đây, padding là string
return padding + value;
}
instanceof Guard
class Dog {
bark() { console.log("Gâu!"); }
}
class Cat {
meow() { console.log("Meo!"); }
}
function makeSound(animal: Dog | Cat) {
if (animal instanceof Dog) {
animal.bark(); // ✅ TypeScript biết là Dog
} else {
animal.meow(); // ✅ TypeScript biết là Cat
}
}
Discriminated Unions — Pattern Matching Kiểu
// Mỗi type có một "discriminator" field duy nhất
type NetworkResult =
| { status: "success"; data: User }
| { status: "error"; error: string }
| { status: "loading" };
function handleNetwork(result: NetworkResult) {
switch (result.status) {
case "success":
console.log(`Tên: ${result.data.name}`); // ✅ result.data tồn tại
break;
case "error":
console.error(`Lỗi: ${result.error}`); // ✅ result.error tồn tại
break;
case "loading":
console.log("Đang tải...");
break;
}
}
in Operator Guard
interface WithId { id: number; }
interface WithName { name: string; }
function process(obj: WithId | WithName) {
if ("id" in obj) {
console.log(`ID: ${obj.id}`); // ✅ obj là WithId
} else {
console.log(`Tên: ${obj.name}`); // ✅ obj là WithName
}
}
Assertion Functions
// Hàm này "khẳng định" param là type T, nếu không thì throw error
function assertIsUser(value: unknown): asserts value is User {
if (typeof value !== "object" || value === null) {
throw new TypeError("Giá trị không phải là object");
}
const user = value as Partial<User>;
if (!user.id || !user.name || !user.email) {
throw new TypeError("Object thiếu property bắt buộc");
}
}
// Sau khi gọi, TypeScript biết type đã được narrow
const rawInput: unknown = { id: 1, name: "An", email: "an@example.com" };
assertIsUser(rawInput);
console.log(rawInput.name); // ✅ TypeScript biết là User
Decorators — Meta-Programming Cho Class
Decorators cho phép bạn thêm metadata hoặc modify behavior của class/method/property:
// Bật decorators trong tsconfig.json:
// "experimentalDecorators": true
function Log(target: any, propertyKey: string, descriptor: PropertyDescriptor) {
const originalMethod = descriptor.value;
descriptor.value = function (...args: any[]) {
console.log(`Gọi method: ${propertyKey}`);
console.log(`Arguments:`, args);
const start = Date.now();
const result = originalMethod.apply(this, args);
const duration = Date.now() - start;
console.log(`Hoàn thành trong ${duration}ms`);
return result;
};
return descriptor;
}
class Calculator {
@Log
add(a: number, b: number): number {
return a + b;
}
@Log
multiply(a: number, b: number): number {
return a * b;
}
}
const calc = new Calculator();
calc.add(5, 3);
// Gọi method: add
// Arguments: [5, 3]
// Hoàn thành trong Xms
Lưu ý: Decorators là tính năng experimental. Từ TypeScript 5.0+, bạn có thể dùng standard decorators (không cần
experimentalDecorators).
Module System Và Import/Export
TypeScript hỗ trợ đầy đủ ES Modules và CommonJS:
// math.ts — Export
export const PI = 3.14159;
export function add(a: number, b: number): number {
return a + b;
}
export default class MathUtils {
static subtract(a: number, b: number): number {
return a - b;
}
}
// app.ts — Import
import MathUtils, { PI, add } from './math';
// Import với alias
import { add as sum } from './math';
// Import namespace
import * as MathModule from './math';
// Dynamic import (lazy loading)
async function loadModule() {
const module = await import('./heavy-module');
return module.default;
}
Tích Hợp Vào Dự Án Thực Tế
Với Astro (như dự án Devs2.org)
Astro hỗ trợ TypeScript native. Chỉ cần:
- Đổi file
.astrothành.astro(đã support TS trong script tag) - Đổi file JS thành TS:
.js→.tshoặc.jsx→.tsx
---
// src/pages/index.astro — TypeScript trong frontmatter
interface Post {
title: string;
slug: string;
date: string;
excerpt: string;
}
const posts: Post[] = [
{
title: "TypeScript Cho Người Mới Bắt Đầu",
slug: "typescript-for-beginners",
date: "2026-08-04",
excerpt: "Học TypeScript từ con số 0..."
}
];
---
<html lang="vi">
<body>
<h1>{posts[0].title}</h1>
</body>
</html>
Với React
// components/UserCard.tsx
interface UserCardProps {
user: {
name: string;
avatar: string;
bio?: string;
};
onFollow: (username: string) => void;
}
const UserCard: React.FC<UserCardProps> = ({ user, onFollow }) => {
return (
<div className="card">
<img src={user.avatar} alt={user.name} />
<h3>{user.name}</h3>
{user.bio && <p>{user.bio}</p>}
<button onClick={() => onFollow(user.name)}>Theo dõi</button>
</div>
);
};
export default UserCard;
Với API Calls
// services/api.ts
interface ApiResponse<T> {
data: T;
status: number;
message: string;
}
async function fetchWithTypes<T>(url: string): Promise<ApiResponse<T>> {
const response = await fetch(url);
if (!response.ok) {
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
}
const data = await response.json();
return {
data,
status: response.status,
message: "Success"
};
}
// Usage — type-safe response
interface User {
id: number;
name: string;
email: string;
}
const { data: user } = await fetchWithTypes<User>('/api/users/1');
console.log(user.name); // ✅ Full autocomplete
Best Practices
1. Bật Strict Mode Ngay Từ Đầu
{
"compilerOptions": {
"strict": true
}
}
Strict mode bật tất cả kiểm tra kiểu nghiêm ngặt nhất. Đây là bắt buộc cho mọi dự án production.
2. Tránh any — Dùng unknown Thay Thế
// ❌ Bad — tắt hoàn toàn type checking
let data: any = fetchData();
data.someMethod(); // Không ai biết someMethod có tồn tại!
// ✅ Good — phải kiểm tra trước khi dùng
let data: unknown = fetchData();
if (typeof data === "object" && data !== null && "name" in data) {
console.log((data as { name: string }).name);
}
3. Ưu Tiên Immutable Data
// ❌ Mutable — dễ gây side effect
function updateUser(user: User, updates: Partial<User>): User {
Object.assign(user, updates);
return user;
}
// ✅ Immutable — an toàn hơn
function updateUser(user: User, updates: Partial<User>): User {
return { ...user, ...updates };
}
4. Viết Type Guards Thay Vì as Casting
// ❌ Unsafe casting — ép kiểu có thể gây lỗi runtime
function getName(input: unknown): string {
return (input as string).toUpperCase(); // Crash nếu input không phải string!
}
// ✅ Safe — kiểm tra trước
function getName(input: unknown): string {
if (typeof input === "string") {
return input.toUpperCase();
}
throw new TypeError("Expected a string");
}
5. Viết Type Definition Cho Third-Party Libraries
// types/external.d.ts
declare module 'external-library' {
export function init(options: {
apiKey: *** debug?: boolean;
timeout?: number;
}): ExternalAPI;
interface ExternalAPI {
fetch(id: string): Promise<Result>;
update(id: string, data: Partial<Result>): Promise<void>;
}
interface Result {
id: string;
value: number;
timestamp: Date;
}
}
Lộ Trình Học TypeScript
Bước 1: Primitive types, arrays, functions ──────── 1-2 ngày
Bước 2: Interfaces, type aliases ────────────────── 2-3 ngày
Bước 3: Generics, utility types ─────────────────── 3-5 ngày
Bước 4: Type narrowing, discriminated unions ────── 2-3 ngày
Bước 5: Tích hợp vào dự án thực tế ──────────────── 1-2 tuần
Bước 6: Advanced (decorators, conditional types) ── Theo nhu cầu
Kết Luận
TypeScript không phải là một ngôn ngữ mới thay thế JavaScript — nó là lớp bảo vệ giúp bạn viết JavaScript tốt hơn. Giống như having a safety net khi biểu diễn trên dây thăng bằng: bạn vẫn làm cùng công việc, nhưng với sự tự tin rằng nếu trượt chân, bạn sẽ không rơi xuống.
Những lợi ích rõ rệt nhất mà bạn sẽ thấy:
- Ít bug hơn — lỗi type bị phát hiện trước khi deploy
- Code dễ đọc hơn — type signature nói lên tất cả về function
- Autocomplete thông minh — tiết kiệm hàng giờ tra cứu documentation
- Refactoring tự tin — đổi tên variable, TypeScript báo cáo tất cả chỗ cần sửa
Hãy bắt đầu với một module nhỏ, dần dần mở rộng ra toàn bộ dự án. Bạn sẽ ngạc nhiên trước năng suất tăng lên chỉ sau vài tuần làm quen.
Chúc bạn viết code an toàn và hiệu quả hơn với TypeScript! 🚀