+23

Bạn có dùng TypeScript như vậy không?

1. Mở bài

Với các project front-end lẫn back-end ngày nay sử dụng JS hay NodeJS thì việc chúng ta sử dụng TypeScript giống như điều hiển nhiên giống như nhìn phải dùng mắt vậy. Nó như một default option khi bạn setup một project mới vậy vì nếu không có nó cảm giác như có gì đó cảm giác không được đúng đúng lắm và hơi bất an khi code ấy. Vì thể hôm nay mình xin chia sẻ với bạn một số cách mà mình đang sử dụng TypeScript trong công việc hàng ngày.

2. Dùng TypeScript như này

a. Tạo type từ một const

Giả sử chúng ta có một constant và một hàm để cập nhật status tương ứng với constant như sau:

const STATUS = {
    IDLE: 'idle',
    LOADING: 'loading',
    SUCCESS: 'success',
    ERROR: 'error'
}

const setStatus = (status: string) => {
    // Some logic
}

Với đoạn code nói trên, mỗi khi ta sử dụng hàm setStatus() ở bất cứ nơi nào trong dự án của mình thì chỉ cần truyền vào nó một string bất kì thì mọi thứ đều hoạt động và TypeScript cũng không than phiền gì chúng ta cả:

setStatus('pending'); // Ok
setStatus('haha'); // Ok

Điều này sẽ dẫn đến bug khi chương trình của chúng ta chạy vì cái giá trị truyền vào hàm setStatus() không như những gì ta mong đợi, không những thế khi đồng nghiệp của bạn cần sử dụng đến hàm này thì cũng đếch biết truyền vào cái gì cho đúng và lại mất công tìm đọc hay đi chất vấn bạn về đoạn code như sh*t ấy. Một cách fix tàm tạm mà bạn có thể làm đó là thay vì sử dụng status: string thì ta có thể thay nó bằng một union type như sau:

type StatusType = 'idle' | 'loading' | 'success' | 'error';

const setStatus = (status: StatusType) => {
    // Some logic
}

Với cách làm như nói trên thì bất cứ khi nào chúng ta hoặc ai khác động đến hàm setStatus() thì sẽ được cái gợi ý như này:

Và việc cần làm lúc này là chỉ cần bấm mũi tên lên xuống để chọn option mà ta mong muốn thôi. Tuy nhiên với cách tiếp cận nói trên thì mỗi khi biến STATUS của chúng ta được cập nhật hoặc thêm mới thì ta sẽ lại phải đồng thời lại phải sửa lại cả ở cái StatusType mà chúng ta định nghĩa trước đó khá là mất công. Vì vậy với các trường hợp như nói trên thì cách mà mình thường hay sử dụng sẽ là như sau:

const STATUS = {
    IDLE: 'idle',
    LOADING: 'loading',
    SUCCESS: 'success',
    ERROR: 'error'
} as const

type StatusType = typeof STATUS[keyof typeof STATUS];

const setStatus = (status: StatusType) => {
    // Some logic
}

setStatus(STATUS.ERROR)

Giải thích qua một chút ở đây ta với dòng code type Status = typeof STATUS[keyof typeof STATUS] ta đang tạo ra một type mới mà giá trị của nó bắt buộc phải là giá trị của các property có trong biến STATUS. Tuy nhiên nếu bạn chỉ dừng lại ở đây thì StatusType của chúng ta sẽ tương đương với dạng string:

Vì bản thân các giá trị của các property trong STATUS đều là dạng string, chính vì vậy mà ở phần khai báo biến STATUS ta cần thêm đoạn as const giống như để nói với TypeScript là các giá trị của biến STATUS này sẽ là dạng constants chứ khôn phải string bất kì. Và bằng cách thêm as const thì đây là những gì chúng ta thu được:

Về sau bất cứ khi nào ta cập nhật biến STATUS dù là thêm mới giá trị hay sử một giá trị thì StatusType của chúng ta sẽ luôn được cập nhật theo, thật tiện lợi hơn phải không nào.

b. Generic type

Giả sử chúng ta có một type và một function như sau:

type User = {
  username: string;
  email: string;
  age: number;
  status: 'active' | 'inactive';
};

const updateUserInfo = (key, value) => {
  // Some logic
}

Với function updateUserInfo() thì ta sẽ mong muốn kiểu dữ liệu của biến key sẽ là một trong các property của type User và đòng thời với mỗi giá trị của key truyền vào thì value sẽ phải là kiểu dữ liệu tương ứng với nó:

key = 'username' => value is string
key = 'age' => value is number
key = 'status' => value is 'active' or 'inactive'

Đầu tiên chúng ta hãy thử làm với cách nói trên vì value mà chúng ta mong muốn sẽ là các giá trị của các property có trong User còn key là các property của User:

type User = {
  username: string;
  email: string;
  age: number;
  status: 'active' | 'inactive';
};

type UserPropertyType = keyof User;
type UserValueType = User[UserPropertyType];

const updateUserInfo = (key: UserPropertyType, value: UserValueType) => {

}

Với cách làm nói trên thì khi ta sử dụng hàm thì sẽ được gợi ý như sau:

Phần key đã giống với kì vọng ban đầu của chúng ta, thế còn value thì sao?

Với cách làm nói trên thì UserValueType sẽ nhận giá trị bất kì miễn nó là string hoặc number giống kiểu dữ liệu của các property trong User vậy nên không thoả mãn được điều chúng ta mong muốn. Đối với các case như mình nêu trên thì mình sẽ sử dụng đến Generic trong TypeScript như sau:

const updateUserInfo = <K extends keyof User>(key: K, value: User[K]) => {
  // Your update logic here...
};

Bạn có thể hiểu nôm na ở đây mình sẽ yêu cầu key có kiểu dữ liệu sẽ luôn là property lấy từ User bằng cách sử dụng từ khoá extends keyof User. Tiếp đến mình sử dụng tiếp type K này luôn cho phần value và yêu cầu nó sẽ có kiểu dữ liệu tương ứng với property K đó bằng từ khoá User[K] (nôm na bạn có thể hiểu ở đây giống như truy cập mảng bình thường nhưng ở đây nó là sử dụng type). Và chỉ bằng cách nói trên đây là kết quả mình thu được:

Ta sẽ luôn nhận được gợi ý tương ứng với key mà chúng ta chọn ngoài ra nếu sau này User của chúng được cập nhật thì tự các giá trị gợi ý sẽ thay đổi theo.

c. Type predicate

Giả sử chúng ta có một đoạn code như sau:

const arr = [1, 'hi', true, 'world', undefined, 'viblo'];

const strArr = arr.filter(item => typeof item === 'string');

const uppercaseStrArr = strArr.map(str => str.toUppercase());

Theo bạn thì nếu dung trong TypeScript thì đoạn code kia có vấn đề gì không? Thử ngừng lăn chuột 30s và tự trả lời nhé.

1
2
3
4
5
...
28
29
30

Và câu trả lời là CÓ VẤN ĐỀ. Mặc dù ở đây chúng ta đã filter chỉ lấy string trong biến arr mà thôi tuy nhiên TypeScript lại bảo khác:

Chính vì điều này khi chúng ta chạy đoạn code cuối cùng sẽ nhận được cái error như sau:

Để xử lý chỗ này thì ta có thể sử dụng tính năng có tên là type predicate như sau:

const arr = [1, 'hi', true, 'world', undefined, 'viblo'];

const isString = (item: unknown): item is string => typeof item === 'string';

const strArr = arr.filter(isString);

const uppercaseStrArr = strArr.map(str => str.toUpperCase());

Bạn để ý sẽ thấy ở đây mình sẽ thêm một hàm filter và đoạn item is string, bằng cách này, ta đang nói cho TypeScript biết rằng dữ liệu trả về từ hàm này sẽ có kiểu dữ liệu là string vì thế đoạn code phía dưới của chúng ta sẽ hoạt động bình thường và không còn cảnh báo lỗi nào từ TypeScript nữa.

Trên thực tế thì mình dùng khá nhiều tính năng nay đóng vai trò làm type check cho các dữ liệu trả về từ GraphQL server như này, giả sử ta có type như trả về từ một GraphQL server:

type Maybe<T> = T | undefined;

type Book = {
    id?: Maybe<string>;
    title?: Maybe<string>;
    publishYear?: Maybe<number>;
}

Tiếp đến ta có một function dùng để hiển thị quyển sách nói trên đã được xuất bản bao nhiêu lâu như sau:

const getNumberOfYearFromPublishYear = (book: Book) => {
  if (book.publishYear) {
    return Math.abs(new Date().getFullYear() - book.publishYear)
  }

  return 'Unknown'
}

Và lại ở đâu đó trong code ta lại có 1 function dùng để kiểm tra 1 quyển sách có phải được xuất bản năm nay hay không, cần trả về Yes nếu được xuất bản trong năm nay, No nếu không được xuất bản trong năm nay và Unknown nếu không có năm xuất bản:

const isPublishThisYear = (book: Book) => {
  if (!book.publishYear) return 'Unknown'

  return book.publishYear === new Date().getFullYear()
}

Như bạn có thể thấy ở đây ta đang code lại đoạn code check xem một quyển sách có thuộc tính publishYear hay không và nếu một ngày nào đó, điều kiện mà chúng ta đang kiểm trả cần thay đổi gì đó thì bạn sẽ phải đi tìm tất các các chỗ mà bạn đang dùng nó để thay đổi. Chưa kể đến trên thực tế sẽ có nhiều điều kiện hơn nhiều. Tuy nhiên nếu bạn chi đơn thuần viết một hàm như sau để check điều nói trên:

const hasPublishYear = (book: Book) => {
    return book.publishYear
}

const getNumberOfYearFromPublishYear = (book: Book) => {
  if (hasPublishYear(book)) {
    return Math.abs(new Date().getFullYear() - book.publishYear)
  }

  return 'Unknown'
}

Với cách làm này ta vẫn sẽ gặp lỗi như sau:

Do ở đây việc ta kiểm tra thuộc tính publishYear nằm ở một function khác nên TypeScript mặc định không thể nhận ra được ta đã check publishYear có tồn tai trước đó rồi nên nó sẽ báo lỗi như nói trên. Tuy nhiên nếu ta sử dụng thêm type predicate thì:

const hasPublishYear = (book: Book): book is Book & { publishYear: number } => {
  return !!book.publishYear;
};

Và TypeScript sẽ không báo lỗi nữa, ngon (Thường bạn nên tạo phần type predicate kia thành kiểu dữ liệu riêng chứ viết như kia sẽ khó tái sử dụng).

3. Kết bài

Bài viết của mình đến đây là kết thúc, cám ơn các bạn đã đọc và đừng quên để lại 1 upvote cho mình nhé.


All rights reserved

Viblo
Hãy đăng ký một tài khoản Viblo để nhận được nhiều bài viết thú vị hơn.
Đăng kí