Các types thường dùng hằng ngày trong Typescript
Đây là các types cơ bản nhưng cũng phổ biến nhất trong Typescript. Một số types khác phức tạp hơn cũng được xây dựng dựa trên những types cơ bản này.
Kiểu dữ liệu nguyên thuỷ (primitives)
Javascript cũng như Typescript hỗ trợ 7 kiểu dữ liệu nguyên thuỷ: string
, number
, bigint
, boolean
, symbol
, null
, undefined
. Tuy nhiên, hằng ngày thường được sử dụng nhiều nhất vẫn là string
, number
và boolean
.
string
đại diện cho các chuỗi ký tự, ví dụ như "Xin chào, Duy PT
".number
đại diện cho các kiểu số. Javascript không hỗ trợ phân biệt int hay float, do đó mọi thứ đơn giản chỉ lànumber
.boolean
đại diện cho kiểu dữ liệu chỉ gồm 2 giá trị:true
hoặcfalse
.
Các kiểu dữ liệu nguyên thuỷ này được Javascript lưu vào Stack memory. Các kiểu dữ liệu khác đều được lưu vào Heap memory.
let str: string = 'Hello Duy PT'
const PI: number = 3.14
const isMarried: boolean = true
Type Annotations dựa trên biến
Thực ra, Typescipt có cơ chế tự suy ra kiểu dữ liệu của một biến khi bạn gán giá trị cho biến.
// Typescript sẽ tự hiểu biến myName có kiểu dữ liệu là string
// Bạn không cần phải chỉ định type cho biến này.
const myName = 'Duy PT'
Mảng - Array
Bạn có thể hiểu nó giống như một cái hộp chứa rất nhiều thứ có thể giống nhau hoặc khác nhau bên trong.
Để định nghĩa types cho array, bạn cần sử dụng từ khoá []
hoặc Array<>
.
const persons: string[] = ['Justin', 'Ronnie', 'Lyam', 'David']
const animals: Array<string> = ['cat', 'dog', 'bird']
const nums: number[] = [1, 2, 3, 4, 5]
Lưu ý, nếu bạn thấy một đoạn code giống vầy
[number, string]
Thì nó là kiểu Tuple
- một cách sử dụng khác đối với array khi mà bạn biết chính xác số lượng phần tử cũng như kiểu dữ liệu của từng phần tử tại từng vị trí cụ thể. Mình sẽ đề cập về kiểu này trong bài Advanced Typescript khác.
any
TypeScript cũng có một loại dữ liệu đặc biệt là any
mà bạn có thể sử dụng khi muốn bỏ qua việc kiểm tra types của dữ liệu.
Khi một giá trị có kiểu dữ liệu là any
, bạn có thể truy cập bất kỳ thuộc tính của nó (sẽ có kiểu dữ liệu là any
), gọi nó như một hàm, gán hoặc gán từ một giá trị có kiểu dữ liệu bất kỳ, hoặc hầu như bất cứ điều gì khác có cú pháp hợp lệ.
let obj: any = { x: 0 };
// Toàn bộ các đoạn code bên dưới đều hợp lệ
// Sử dụng `any` sẽ bỏ qua toàn bộ quá trình kiểm tra kiểu dữ liệu của Typescript
obj.foo();
obj();
obj.bar = 100;
obj = "hello";
const n: number = obj;
Đại loại là cái gì cũng được :D cá nhân mình hầu như không bao giờ dùng kiểu dữ liệu này, bởi nếu lạm dụng kiểu này thì vứt mẹ nó Typescript đi, dùng làm gì nữa :D
Functions
Đây là thứ mà ai cũng phải sử dụng hằng ngày khi code Javascript. Typescript cho phép chúng ta chỉ định kiểu dữ liệu cho các params cũng như output của function.
const sum: (a: number, b: number) => number = (a, b) => a + b
sum(1, 2) //3
sum ('hello', 2)
// Argument of type 'string' is not assignable to parameter of type 'number'
function greeting(yourName: string): string {
return `Hello, ${yourName.toUpperCase()}!!`
}
greeting('Duy PT') // Hello, Duy PT!!
greeting(15)
// Argument of type 'number' is not assignable to parameter of type 'string'
Anonymous Functions
Các hàm ẩn danh (Anonymous functions) khác một chút so với các khai báo hàm thông thường. Khi một hàm xuất hiện tại một vị trí mà TypeScript có thể xác định được cách nó sẽ được gọi, các tham số của hàm đó sẽ tự động được gán kiểu.
// Không khai báo types, nhưng TypeScript vẫn check ra được type đúng cho biến "names"
const names = ["Alice", "Bob", "Eve"]; // string[]
// Kiểu ngữ cảnh cho hàm
names.forEach(function (s) {
console.log(s.toUppercase());
// Property 'toUppercase' does not exist on type 'string'. Did you mean 'toUpperCase'?
});
// Kiểu ngữ cảnh cũng được áp dụng cho arrow functions
names.forEach((s) => {
console.log(s.toUppercase());
// Property 'toUppercase' does not exist on type 'string'. Did you mean 'toUpperCase'?
});
Mặc dù tham số s
không có chú thích kiểu dữ liệu, TypeScript sử dụng các kiểu của hàm forEach
, cùng với kiểu được suy ra của mảng names
, để xác định kiểu dữ liệu mà s
sẽ có.
Quá trình này được gọi là kiểu ngữ cảnh (contextual typing) vì ngữ cảnh mà hàm xuất hiện trong thông báo cho TypeScript kiểu dữ liệu nên có.
Điều này giúp bạn không phải khai báo kiểu dữ liệu khi mà chúng không thực sự cần thiết không cần thiết.
Object Types
Ngoài kiểu dữ liệu nguyên thủy, kiểu dữ liệu phổ biến nhất mà bạn sẽ gặp phải là kiểu đối tượng (object type).
Để định nghĩa một kiểu đối tượng, chúng ta đơn giản chỉ cần liệt kê các thuộc tính và kiểu của chúng.
Ví dụ, đây là một hàm nhận đối tượng Pointer
:
function printCoord(pointer: { lat: number; lng: number }) {
console.log("The latitude value is " + pointer.lat);
console.log("The longitude value is " + pointer.lng);
}
printCoord({ lat: 3, lng: 7 });
Ở đây, chúng ta đã khai báo tham số với một loại dữ liệu có hai property - lat
và lng
- cả hai đều có kiểu dữ liệu là number
. Bạn có thể sử dụng dấu ,
hoặc ;
để phân tách các property và dấu phân tách cuối cùng có thể có hoặc không.
Việc khai báo kiểu dữ liệu cho mỗi property cũng có thể có hoặc không. Nếu bạn không chỉ định loại dữ liệu, nó sẽ được giả định là any
.
Optional Properties
Chúng ta có thể khai báo một số property có thể có hoặc không. Để làm điều này, thêm dấu ?
ngay sau tên thuộc tính:
function printName(obj: { first: string; last?: string }) {
// ...
}
printName({ first: "Bob" });
printName({ first: "Alice", last: "Alisson" });
Trong JavaScript, nếu bạn truy cập vào một thuộc tính không tồn tại, bạn sẽ nhận được giá trị undefined
thay vì một lỗi runtime. Do đó, khi bạn đọc từ một optional property, bạn sẽ phải kiểm tra xem có undefined
trước khi sử dụng nó.
function printName(obj: { first: string; last?: string }) {
// Error - Object is possibly 'undefined'
console.log(obj.last.toUpperCase());
if (obj.last !== undefined) {
// OK
console.log(obj.last.toUpperCase());
}
// Một cách mới và an toàn hơn đó là sử dụng optional chaining
console.log(obj.last?.toUpperCase());
}
Union Types
Hệ thống kiểu dữ liệu của TypeScript cho phép bạn xây dựng các types mới từ các types hiện có bằng cách sử dụng nhiều toán tử khác nhau. Chúng ta đã biết cách khai báo một vài types, giờ chúng ta sẽ kết hợp chúng lại bằng một cách khá thú vị - Union Type.
Cách đầu tiên để kết hợp các loại mà bạn có thể thấy là một Union Type.
Một Union Type là một type được tạo thành từ hai hoặc nhiều types khác nhau, đại diện cho việc các giá trị có thể là một trong những loại được khai báo trong đó. Chúng ta tham chiếu đến mỗi type này là các thành viên của Union type.
Hãy viết một hàm có thể hoạt động trên các string
hoặc number
:
function printId(id: number | string) {
console.log(`ID is ${id}`);
}
// OK
printId(101);
// OK
printId("202");
// Argument of type '{ myID: number; }' is not assignable to parameter of type 'number | string'.
printId({ myID: 22342 });
Việc cung cấp một giá trị phù hợp với một loại union rất đơn giản - chỉ cần cung cấp một type phù hợp với bất kỳ thành viên nào của union đó. Nếu bạn có một giá trị của một union type, làm thế nào để bạn làm việc với nó?
TypeScript chỉ cho phép một hoạt động nếu nó hợp lệ cho mỗi thành viên của union. Ví dụ, nếu bạn có một union string | number
, bạn không thể sử dụng các methods chỉ có sẵn trên string:
function printId(id: number | string) {
console.log(id.toUpperCase());
}
// Error
// Property 'toUpperCase' does not exist on type 'string | number'.
// Property 'toUpperCase' does not exist on type 'number'.
Giải pháp là thu hẹp union bằng code, giống như bạn làm trong JavaScript khi mà không có khai báo kiểu dữ liệu. Lúc này, TypeScript có thể suy ra một type cụ thể hơn cho giá trị dựa trên cấu trúc của code.
function printId(id: number | string) {
if (typeof id === "string") {
console.log(id.toUpperCase()); // string
} else {
console.log(id); // number
}
}
function welcomePeople(x: string[] | string) {
if (Array.isArray(x)) {
console.log("Hello, " + x.join(" and ")); // string[]
} else {
console.log("Welcome lone traveler " + x); // string
}
}
Type Aliases
Type alias chỉ đơn giản là việc khai báo một tên chung cho bất kỳ kiểu dữ liệu nào.
type Person = {
name: string;
age: number;
}
Để sử dụng type alias, bạn có thể sử dụng nó trong các type annotation khác nhau. Ví dụ, bạn có thể sử dụng type alias Person
đã định nghĩa trong type annotation cho một function như sau:
function greet(person: Person): string {
return `Hello, ${person.name}!`
}
Bạn cũng có thể sử dụng type alias để định nghĩa một union type. Ví dụ, bạn có thể sử dụng type alias Animal
để định nghĩa một union type bao gồm cả Dog
và Cat
như sau:
type Animal = Dog | Cat;
Khả năng kế thừa
type Animal = {
name: string
}
type Bear = Animal & {
honey: boolean
}
Interfaces
interface
là một cách khác để định nghĩa một object type. Cú pháp để định nghĩa một interface
là:
interface InterfaceName {
<propertyName>: <type>;
...
}
Ví dụ, bạn có thể sử dụng cú pháp trên để định nghĩa một interface
cho một object type như sau:
function greet(person: Person): string {
return `Hello, ${person.name}!`;
}
Khả năng kế thừa
interface Animal {
name: string
}
interface Bear extends Animal {
honey: boolean
}
Sự khác biệt giữa Type Alias và Interface
Một số khác biệt khác giữa type alias và interface là type
alias không thể được mở lại để thêm các thuộc tính mới, trong khi interface
luôn luôn có thể được mở rộng. Ví dụ, bạn có thể sử dụng interface Person
để thêm thuộc tính mới như sau:
interface Person {
name: string;
age: number;
}
interface Person {
address: string;
}
Trong ví dụ này, Person
đã trở thành một kiểu mới, gồm 3 properties: name
, age
và address
.
Type Assertions
Đôi khi bạn có thông tin về type của một giá trị mà TypeScript không thể biết được.
// Lúc này, kiểu dữ liệu mặc định sẽ luôn là HTMLElement
const canvasElement: HTMLElement = document.getElementById('canvas')
Khi bạn muốn chỉ định cho Typescript biết canvasElement
chính xác là một HTMLCanvasElement
.
const canvasElement = document.getElementById('canvas') as HTMLCanvasElement;
TypeScript chỉ cho phép sử dụng type assertions để chuyển đổi sang một phiên bản cụ thể hơn hoặc ít cụ thể hơn của một type. Quy tắc này ngăn chặn các hành vi ép kiểu giữa 2 types không liên quan đến nhau:
let someValue: string = 'this is a string';
// Error!
// Conversion of type 'string' to type 'number' may be a mistake because neither type sufficiently overlaps with the other. If this was intentional, convert the expression to 'unknown' first.
let numb: number = someValue as number;
Đôi khi bạn thấy quy tắc này quá phức tạp, và ok, bạn biết rõ điều cần làm là không có vấn đề gì cả. Lúc này, bạn có thể sử dụng cú pháp ép kiểu về unknown
hoặc any
trước.
let numb: number = someValue unknown as number;
let numb: number = someValue any as number;
Mình khuyên bạn nên sử dụng unknown
vì đôi khi project không cho phép sử dụng any
khi thiết lập noImplicitAny=true
trong tsconfig.json
.
Enums
Enum là một tính năng được thêm vào bởi TypeScript để mô tả một giá trị có thể là một trong một tập hợp các hằng số được đặt tên.
Khác với hầu hết các tính năng khác của TypeScript, đây là một bổ sung vào JavaScript kể cả lúc compile time lẫn runtime.
enum Direction {
Up = 'Up',
Down = 'Down',
Left,
Right,
Other = 1,
...
}