Các Types (kiểu dữ liệu) trong TypeScript (P3)

Tác giả: Trần Anh Tuấn

Tiếp nối ở bài trước, chúng ta đã học được cũng nhiều kiến thức về Typescript rồi. Ở bài viết này chúng ta sẽ cùng tìm hiểu thêm về nhiều Types hay ho khác của Typescript như unionintersectionutilitiy,…

Lưu ý khi đặt tên Type hoặc Interface thì nên đặt tên dễ hiểu và chữ cái đầu IN HOA nhé. Ví dụ như Permissions, UserName, Role…

>> TypeScript là gì? Tại sao nên chọ TypeScript? << Đọc bài viết này để giải đáp

Union Type

Mình muốn tạo ra một Type có tên là Role để chứa các quyền của người dùng như là AdminGuest, và User. Người dùng sẽ có 1 trong 3 quyền này cho nên chúng ta sẽ viết nó như sau

type Role = "Admin" | "User" | "Guest";

Ngoài ra Union Type còn dùng khi khai báo kiểu dữ liệu cho biến mà mình có đề cập đến cho các bạn ở những bài trước như là

let age: number | string = '5';

Đi sâu hơn vào nó thì sẽ có một vài trường hợp như sau để các bạn nhớ. Vì là Union nghĩa là hoặc Type này hoặc Type kia.

type NetworkLoadingState = {
  state: "loading";
};
type NetworkFailedState = {
  state: "failed";
  code: number;
};
type NetworkState =
  | NetworkLoadingState
  | NetworkFailedState;
const state: NetworkState = {
    state: "loading"
  };
state.code = {};
// Property 'code' does not exist on type 'NetworkLoadingState'.

Mình khai báo 2 Types là NetworkLoadingState và NetworkFailedState, sau đó mình khai báo tiếp 1 Type là NetworkState là Union Type. Khi mình dùng const state: NetworkState thì mình sử dụng được là state bởi vì 2 Types đều có chung key là state.

Tuy nhiên khi mình dùng state.code thì lại báo lỗi, mình có ghi là bởi vì Union là hoặc cho nên có thể là nó chạy vào NetworkLoadingState, mà thằng này thì không có key là code.

function printId(id: number | string) {
  console.log(id.toUpperCase());
// Property 'toUpperCase' does not exist on type 'string | number'.
// Property 'toUpperCase' does not exist on type 'number'.
}

Một ví dụ khác khi khai báo params cho function và sử dụng các phương thức dựa vào params. Nếu là string thì có thể sử dụng .toUpperCase() tuy nhiên nếu là number thì sẽ không có các phương thức đó. Cho nên chương trình sẽ bắn ra lỗi ngay lập tức.

Để giải quyết vấn đề đó thì có thể kiểm tra Type của params dựa vào typeof như thế này

function printId(id: number | string) {
  if (typeof id === "string") {
    // In this branch, id is of type 'string'
    console.log(id.toUpperCase());
  } else {
    // Here, id is of type 'number'
    console.log(id);
  }
}

Intersection Type

Ngược lại với Union Type đó chính là Intersection Type(& và). Và Type này và Type kia. Nghĩa là sử dụng được hết toàn bộ các keys từ nhiều Types luôn.

type Student = {
  name: string;
  age: number;
}
type Person = {
  name: string;
}
type People = Person & Student;
const people: People = {
  name: 'xiaoming',
}
// Property 'age' is missing in type '{ name: string; }' but required in type 'Student'.

Như ví dụ trên thì các bạn sẽ thấy mình không truyền vào key là age nên chương trình sẽ báo lỗi ngay vì nó bắt buộc khi sử dụng Intersection Type. Một điều nữa là phải nhất quán khi khai báo nhé, chú ý age là number nhưng lại sử dụng là string cũng sẽ lỗi.

const people: People = {
  name: 'xiaoming',
  age: '24'
}
// Type 'string' is not assignable to type 'number'.

Một điều cuối, cẩn thận khi khai báo như thế này, khi các bạn không xác định Type cho key mà là một giá trị nào đó thì 2 Type đó không được trùng key, nếu không thì nó sẽ trả ra never, mà never chúng ta đã học ở bài phần 1 rồi.

Type never không gán được bất kỳ giá trị nào!

type Student = {
  name: 'evondev';
}
type Person = {
  name: 'tuanpzo';
}
type People = Person & Student; // never
// Error
const people: People = {
  name: 'xiaoming',
}

Tham khảo việc làm Typescript hấp dẫn tại TopDev

Utility Types

Utility types thì có rất nhiều tuy nhiên ở đây mình sẽ liệt kê kèm ví dụ minh họa cho các bạn những cái thông dụng thôi nhé.

Partial<Type>

Nó sẽ tạo ra một Type mới dựa vào Type gốc nhưng sẽ thay đổi toàn bộ keys thành optional(không bắt buộc dấu ?)

type Todo = {
  title: string;
  description: string;
};

function updateTodo(todo: Todo, fieldsToUpdate: Partial<Todo>) {
 return { ...todo, ...fieldsToUpdate };
}
updateTodo({ title: "test", description: "" }, { description: "test" });

Mình có type là Todo với 2 keys là bắt buộc, các bạn để ở chỗ fieldsToUpdate mình có dùng Partial<Todo> cho nên param fieldsToUpdate lúc này nó sẽ trông như code ở dưới.

Cho nên khi mình gọi hàm updateTodo thì param đầu tiên buộc phải đầy đủ title và description, tuy nhiên param thứ 2 thì mình chỉ cần truyền description là được rồi, không báo lỗi gì cả.

type Todo = {
  title?: string;
  description?: string;
};

Required<Type>

Ngược lại với Partial<Type> ở trên thì thằng này sẽ biến các keys thành bắt buộc, cho dù ban đầu nó là optional(?) đi chăng nữa

type Todo = {
  title?: string;
  description?: string;
};

function updateTodo(todo: Todo, fieldsToUpdate: Required<Todo>) {
  return { ...todo, ...fieldsToUpdate };
}
updateTodo({ title: "test", description: "" }, { description: "test" } Error);
// Property 'title' is missing in type '{ description: string; }' but required in type 'Required<Todo>'

Record<Keys, Type>

Nó sẽ tạo ra Type có các Keys và Value. Mình sẽ nói trường hợp đơn giản trước cho các bạn như này, nhìn vào rất dễ hiểu đúng không ? Keys là string và Value là Number.

const example: Record<string, number> = {
  a: 1,
  b: 2,
  c: 3,
};

Tiếp tục mình khai báo 2 Type tương ứng là CatInfo và CatName sau đó mình lại sử dụng Record để tạo ra các thông tin cho các chú mèo như sau. Lúc này từng chú mèo(CatName) sẽ có các thông tin như nhau(CatInfo) là age và breed. Quá xịn phải không nào.

type CatInfo = {
  age: number;
  breed: string;
}

type CatName = "miffy" | "boris" | "mordred";

const cats: Record<CatName, CatInfo> = {
  miffy: { age: 10, breed: "Persian" },
  boris: { age: 5, breed: "Maine Coon" },
  mordred: { age: 16, breed: "British Shorthair" },
};

Readonly<Type>

Như tên gọi của nó, sẽ làm cho các keys trở nên Readonly(chỉ đọc chứ không được sửa). Nhìn ví dụ cho dễ thông não nè.

Interface cũng tương tự như Type thôi. Mình sẽ nói chi tiết về sự khác nhau giữa chúng ở bài sau nhé.

interface Todo {
  title: string;
}

const todo: Readonly<Todo> = {
  title: "Delete inactive users",
};

todo.title = "Hello";
// Cannot assign to 'title' because it is a read-only property.

Pick<Type, Keys>

Pick nghĩa là chọn ra những keys mà các bạn muốn từ một Type nào đó. Đôi khi trong quá trình code có rất nhiều keys từ Type, nhưng chúng ta chỉ sử dụng vài cái trong đó, lúc này Pick<Type> là một lựa chọn tuyệt vời.

Mình lấy ra 2 keys là title và completed từ interface Todo

interface Todo {
  title: string;
  description: string;
  completed: boolean;
}

type TodoPreview = Pick<Todo, "title" | "completed">;

const todo: TodoPreview = {
  title: "Clean room",
  completed: false,
};

Omit<Type, Keys>

Ngược lại với Pick<Type, Keys> thì Omit<Type, Keys> sẽ lấy toàn bộ các keys từ Type nào đó sau đó loại bỏ ra một số keys(string hoặc union) không cần thiết.

interface Todo {
  title: string;
  description: string;
  completed: boolean;
  createdAt: number;
}

type TodoPreview = Omit<Todo, "description">;
// remove description
const todo: TodoPreview = {
  title: "Clean room",
  completed: false,
  createdAt: 1615544252770,
};

Extract<Type, Union>

Dùng để trích xuất Type từ Union Type

type T0 = Extract<"a" | "b" | "c", "a" | "f">; 
// type T0 = "a"
type T1 = Extract<string | number | (() => void), Function>; 
// type T1 = () => void

type Shape =
  | { kind: "circle"; radius: number }
  | { kind: "square"; x: number }
  | { kind: "triangle"; x: number; y: number };

type T2 = Extract<Shape, { kind: "circle" }>
// type T2 = { kind: "circle"; radius: number;}

Exclude<UnionType, ExcludedMembers>

Ngược lại với Extract ở trên thì nó sẽ loại bỏ những ExcludedMembers từ UnionType

// remove a
type T0 = Exclude<"a" | "b" | "c", "a">;
// type T0 = "b" | "c"

// remove a and b
type T1 = Exclude<"a" | "b" | "c", "a" | "b">; 
// type T1 = "c"

// remove Function
type T2 = Exclude<string | number | (() => void), Function>;
// type T2 = string | number;

NonNullable<Type>

Dùng để loại bỏ undefined và null ra khỏi Type

type T0 = NonNullable<string | number | undefined>;
// type T0 = string | number;

Tạm kết

Phần 3 tạm dừng ở đây nhé các bạn. Ở bài này chúng ta đã học thêm được khá nhiều Types mới rồi, kiến thức cũng có thêm 1 chút. Ở bài số 4 chúng ta sẽ tìm hiểu sự biệt giữa interface và type, cũng như tìm hiểu thêm về Mapped và Index Type cùng một số kiến thức liên quan tới chúng.

Bài viết gốc được đăng tải tại evondev.com

Nhiều bài học TypeScript hơn tại đây:

Xem thêm Việc làm IT hấp dẫn trên TopDev