Typescript dành cho JS developers
Bài viết này cung cấp một cái nhìn tổng quan ngắn gọn về TypeScript, tập trung vào hệ thống kiểu dữ liệu của nó.
Awesome TypeScript
Typescript là gì?
Một phiên bản Javascript++
TypeScript là một ngôn ngữ mã nguồn mở dựa trên JavaScript, một trong những ngôn ngữ được sử dụng nhiều nhất trên thế giới. Typescript mở rộng thêm Javascript bằng cách thêm vào một số static types.
Types cung cấp một phương thức tường minh hơn để mô tả các hình thái của object, diễn giải documentation tốt hơn, thông qua đó TypeScript có thể xác định rằng code của bạn đang hoạt động chính xác hay không.
Chỉ định Types là không bắt buộc trong TypeScript.
Đáng tin cậy
Tất cả code JavaScript hợp lệ cũng sẽ hợp lệ với TypeScript. Typecript không bắt lỗi kiểu dữ liệu trong lúc chạy run-time. Điều đó có nghĩa là bạn có thể viết code không mắc lỗi kiểu dữ liệu, nhưng bạn có thể sẽ gặp lỗi lúc thực thi. Bạn phải hết sức cẩn thận với dữ liệu bên ngoài thu được thông qua các ajax request, hay từ các hệ thống files, storage (localStorage, sessionStorage...).
Code TypeScript được chuyển thành code JavaScript thông qua trình biên dịch TypeScript hoặc Babel. Các mã JavaScript đã được biên dịch này sẽ clean, đơn giản, chạy ở tất cả mọi nơi cần JavaScript: trình duyệt web, Node.JS hoặc trong các ứng dụng của bạn.
Có thể áp dụng từ từ
Việc áp dụng TypeScript không phải là thay đổi đột ngột. Bạn có thể bắt đầu bằng cách chú thích JavaScript hiện có bằng JSDoc, sau đó chuyển dần một vài files sang TypeScript. Cứ thực hiện mọi thứ một cách từ từ cho tới khi codebase của bạn được chuyển đổi hoàn toàn.
Với tính năng chỉ định kiểu dữ liệu của TypeScript, bạn không cần phải chú thích code của mình trừ khi bạn muốn an toàn hơn ^_^.
TypeScript dành cho JavaScript developers
TypeScript có mối quan hệ hiếm có với JavaScript. TypeScript cung cấp tất cả các tính năng sẵn có của JavaScript cùng với một lớp bổ sung: Hệ thống định dạng kiểu dữ liệu của TypeScript.
Ví dụ: JavaScript cung cấp các kiểu dữ liệu nguyên thủy như string, number và object, nhưng nó không thể kiểm tra xem bạn đã gán chúng một cách nhất quán hay chưa. TypeScript thì có.
Điều này có nghĩa là code JavaScript đang hoạt động hiện tại của bạn cũng là code TypeScript. Lợi ích chính của TypeScript là nó có thể làm nổi bật các behaviour không mong muốn trong code, từ đó giảm nguy cơ gây ra lỗi.
Bài viết này cung cấp một cái nhìn tổng quan ngắn gọn về TypeScript, tập trung vào hệ thống kiểu dữ liệu của nó.
Suy đoán kiểu (Types by Inference)
TypeScript sẽ tự động tạo ra các kiểu dữ liệu tương ứng cho bạn trong nhiều trường hợp. Ví dụ: khi bạn tạo một biến và gán nó cho một giá trị cụ thể, TypeScript sẽ sử dụng giá trị của biến làm kiểu dữ liệu của nó.
let helloWorld = "Hello World"
Lúc này kiểu dữ liệu của helloWorld
sẽ là string. Bạn cũng có thể viết một cách tường minh hơn như sau:
let helloWorld: string = "Hello World"
Dựa trên cách hoạt động của JavaScript, TypeScript có thể xây dựng một hệ thống chấp nhận code JavaScript nhưng có thêm types (mình hay gọi đùa là có thai haha). Điều này cung cấp một hệ thống kiểu dữ liệu mà không cần thêm các ký tự bổ sung để tạo kiểu tường minh trong code hiện tại. Đó là cách TypeScript biết rằng helloWorld
là một string trong ví dụ đầu tiên.
Chỉ định kiểu (Defining types)
TypeScript cung cấp một cú pháp mở rộng giúp bạn có thể chỉ định chính xác kiểu dữ liệu mà bạn mong muốn.
Ví dụ: để tạo một object với suy đoán kiểu tự động bao gồm name là string và id là number, bạn có thể viết như sau:
const user = {
name: "Hayes",
id: 0,
}
Một cách thức xịn xò hơn là khai báo một interface:
interface User {
name: string
id: number
}
Sau đó, bạn có thể khai báo một object JavaScript mới, đồng thời chỉ định kiểu dữ liệu cho object này:
const user: User = {
name: "Hayes",
id: 0,
}
Nếu bạn cung cấp một object không phù hợp với interface bạn chỉ định, TypeScript sẽ xuất ra một cảnh báo:
const user: User = {
username: "Hayes",
id: 0,
}
Type '{ username: string; id: number; }' is not assignable to type 'User'. Object literal may only specify known properties, and 'username' does not exist in type 'User'.
Bạn cũng có thể khai báo kiểu dữ liệu cho các class:
interface User {
name: string
id: number
}
class UserAccount {
name: string
id: number
constructor(name: string, id: number) {
this.name = name
this.id = id
}
function getAdminUser(): User {
//...
}
function deleteUser(user: User) {
// ...
}
}
const user: User = new UserAccount("Murphy", 1)
Javascript hỗ trợ một số kiểu dữ liệu nguyên thủy như: boolean
, bigint
, null
, number
, string
, symbol
, object
, và undefined
. Bạn hoàn toàn có thể sử dụng các kiểu dữ liệu này khi khai báo interface. Typescript cũng hỗ trợ thêm một số kiểu mới:
void
: chấp nhận giá trịnull
hoặcundefined
. Thường được dùng khi khai báo một function mà không trả về giá trị nào.any
: cái gì cũng được. Typescript sẽ không kiểm tra kiểu dữ liệu khi gặp loại này.unknown
: tương tự nhưany
, bạn có thể gán giá trị với bất cứ kiểu dữ liệu gì. Tuy nhiên, nó có vài điểm khác biệt.
Nếu như any
cho phép thực hiện bất kỳ operation nào mà không kiểm tra kiểu dữ liệu thì uknown
lại gần như không cho phép thực hiện operation nào.
let myValue: unknown
myValue.bar() // Error
myValue.toString() // Error
myValue[0] // Error
Chúng ta cần sử dụng type-checking để có thể thực hiện các operation trên unknown
:
let myVariable: unknow
if (typeof value === 'number') {
myVariable++
}
if (value instanceOf Xyz) {
myVariable.foo()
}
never
: cái kiểu này thì hơi bựa, nó đại diện cho kiểu dữ liệu có giá trị không bao giờ xảy ra =)) (nghe hơi lùng bùng phải không ^_^).
Ví dụ: hàm dưới đây luôn trả về một Exception, nên chẳng khi nào nó trả ra giá trị gì (vì code chạy tới đây là dừng cmnr), do đó kiểu dữ liệu ở đây sẽ là never
.
let showError = (message: string): never => {
throw new Error(message)
}
Tự định nghĩa kiểu dữ liệu mới
Với TypeScript, bạn có thể tạo các kiểu phức tạp bằng cách kết hợp lại các kiểu đơn giản. Có hai cách phổ biến để làm như vậy đó là Unions và Generics.
Unions
Bạn có thể chỉ định một type mới chấp nhận giá trị là một trong một số types xác định nào đó.
Ở ví dụ sau, bạn chỉ định một kiểu dữ liệu boolean chỉ chấp nhận 1 trong 2 giá trị là true hoặc false.
type MyBool = true | false
Một vài ví dụ khác cho bạn dễ hình dung:
type WindowStates = "open" | "closed" | "minimized"
type LockStates = "locked" | "unlocked"
type OddNumbersUnderTen = 1 | 3 | 5 | 7 | 9
Unions cũng cung cấp một cách thức để xử lý các kiểu dữ liệu khác nhau. Ví dụ, bạn có thể khai báo một function nhận giá trị là một string hoặc mảng string:
function getLength(obj: string | string[]) {
return obj.length
}
Generics
Generics cung cấp các variables cho types. Một ví dụ phổ biến là array. Một array không chỉ định generics có thể chứa bất kỳ thứ gì. Một array được chỉ định generics giúp mô tả các giá trị mà array đó chứa.
type StringArray = Array<string>
type NumberArray = Array<number>
type ObjectWithNameArray = Array<{ name: string }>
Bạn cũng có thể khai báo một kiểu của riêng bạn thông qua generics
interface User {
nane: string
id: number
}
// UserArray là một kiểu mới, có giá trị là một mảng các User.
type UserArray = Array<User>
interface Backpack<Type> {
add: (obj: Type) => void
get: () => Type
}
// Đoạn code này giúp Typescript biết rằng
// có một hằng số là `backpack`, và không cần quan tâm nó được khai báo ở đâu.
declare const backpack: Backpack<string>
// object là một string, bởi vì chúng ta đã khai báo nó ở interface trên là phần biến của Backpack.
const object = backpack.get()
// Vì biến backpack là một chuỗi, bạn không thể đưa một number vào hàm add().
backpack.add(23) //Argument of type 'number' is not assignable to parameter of type 'string'.
Hệ thống kiểu dữ liệu có cấu trúc (Structural Type System)
Một trong những nguyên tắc cốt lõi của TypeScript là kiểm tra kiểu dữ liệu bằng cách tập trung vào hình thái (shape) mà các giá trị (values) có. Điều này được gọi là "duck typing" hay "structural typing".
Trong một hệ thống kiểu dữ liệu có cấu trúc, nếu 2 object có hình thái giống nhau thì chúng được xem là cùng type.
Ở ví dụ bên dưới, biến point
được xem là thuộc kiểu Point
.
interface Point {
x: number
y: number
}
function logPoint(p: Point) {
console.log(`${p.x}, ${p.y}`)
}
// logs "12, 26"
const point = { x: 12, y: 26 }
logPoint(point)
Biến point
tuy chưa bao giờ được khai báo là một kiểu Point
nhưng TypeScript so sánh hình thái của point
với hình thái của kiểu Point
trong quá trình kiểm tra kiểu dữ liệu. Chúng có hình thái giống nhau, vì vậy code hợp lệ.
Việc so khớp hình thái chỉ yêu cầu một tập hợp các trường của object cần so sánh.
const point3 = { x: 12, y: 26, z: 89 }
logPoint(point3) // logs "12, 26"
const rect = { x: 33, y: 3, width: 30, height: 80 }
logPoint(rect) // logs "33, 3"
const color = { hex: "#187ABF" }
logPoint(color) //Argument of type '{ hex: string; }' is not assignable to parameter of type 'Point'. Type '{ hex: string; }' is missing the following properties from type 'Point': x, y
Một ví dụ khác sử dụng class:
class VirtualPoint {
x: number
y: number
constructor(x: number, y: number) {
this.x = x
this.y = y
}
}
const newVPoint = new VirtualPoint(13, 56)
logPoint(newVPoint) // logs "13, 56"
Nếu object hoặc class có tất cả các property bắt buộc, TypeScript sẽ coi chúng là cùng loại, bất kể cách thức triển khai là khác nhau hay không.