殺手級的TypeScript功能:const斷言[每日前端夜話0x6F]

殺手級的TypeScript功能:const斷言[每日前端夜話0x6F]

瘋狂的技術宅 前端先鋒

每日前端夜話0x6F
每日前端夜話,陪你聊前端。
每天晚上18:00準時推送。
正文共:1916 字
預計閱讀時間: 6 分鐘
翻譯:瘋狂的技術宅
來源:logrocket





殺手級的TypeScript功能:const斷言[每日前端夜話0x6F]
我發現官方的 TypeScript 文檔非常有用,但是總覺得有點過於學術化並且枯燥無味。每當我發現一個新功能時,我想要知道這個功能究竟能夠解決什麼問題而不是長篇大論。

在我看來,const assertions 是 TypeScript 3.4 的殺手級新功能,正如我稍後將要解釋的,我們可以用這個新功能省略很多繁瑣的類型聲明。

const 斷言



1const x = { text: "hello" } as const;

官方文檔中給出了這樣的解釋:

TypeScript 3.4 引入了一個名爲 const 斷言的字面值的新構造。它的語法是一個類型斷言,用 const 代替類型名稱(例如 123 as const)斷言構造新的文字表達式時,我們可以向語言發出以下信號:
該表達式中的字面類型不應被擴展(例如:不能從“hello”轉換爲字符串)
對象字面量獲取只讀屬性
數組文字成爲只讀元組


感覺有點枯燥,還有點混亂。讓我們來各個擊破。

沒有類型擴展的字面類型


並不是每個人都知道類型擴展,並且由於某些意外行爲而首次發現它時都會覺得意外。

當我們使用關鍵字 const 聲明一個字面量時,類型是等號右邊的文字,例如:


1const x = 'x'; // x has the type 'x'

const 關鍵字確保不會發生對變量進行重新分配,並且只保證該字面量的嚴格類型。

但是如果我們用 let 而不是 const, 那麼該變量會被重新分配,並且類型會被擴展爲字符串類型,如下所示:


1let x = 'x'; // x has the type string;

以下是兩個不同的聲明:


1const x = 'x'; // has the type 'x' 
2let y = 'x';   // has the type string

y 被擴展爲更通用的類型,並允許將其重新分配給該類型的其他值,而變量 x 只能具有 'x'的值。

用新的 const 功能,我可以這樣做:


1let y = 'x' as const; // y has type 'x'`

對象字面量獲取只讀屬性


在 Typescript 3.4 之前,類型擴展發生在對象字面量中:


1const action = { type: 'INCREMENT', } // has type { type: string }

即使我們將 action 聲明爲 const,仍然可以重新分配 type 屬性,因此,該屬性被擴展成了字符串類型。

這看上去令人覺得不是那麼有用,所以讓我們換一個更好的例子。

如果你熟悉 Redux,就可能會發現上面的 action 變量可以用作 Redux action。如果你不知道 Redux 我來簡單解釋一下,Redux 是一個全局不可變的 state 存儲。通過向所謂的 reducers 發送動作來修改狀態。 reducers 是純函數,它在調度每個 action 後返回全局狀態的新更新版本,以反映 acion 中指定的修改。

在 Redux 中,標準做法是從名爲 action creators 的函數創建操作。 action creators 只是純函數,它返回 Redux操作對象字面量以及提供給函數的所有參數。

用一個例子可以更好地說明這一點。應用程序可能需要一個全局 count 屬性,爲了更新這個 count 屬性,我們可以調度類型爲 'SET_COUNT' 的動作,它只是將全局 count 屬性設置爲一個新的值,這是一個字面對象屬性。這個 action 的 action creator 將是一個函數,它接受一個數字作爲參數,並返回一個具有屬性爲 type、值爲 SET_COUNT 和類型爲 number 的 payload 屬性的對象,它將指定 count 的新值:


 1const setCount = (n: number) => {
 2  return {
 3    type: 'SET_COUNT',
 4    payload: n,
 5  }
 6}
 7
 8const action = setCount(3)
 9// action has type
10// { type: string, payload: number }

從上面顯示的代碼中可以看出,type 屬性已經被擴展爲 string 類型而不再是 SET_COUNT。這不是很安全的類型,我們可以保證的是 type 是一個字符串。 redux 中的每個 action 都有一個 type 屬性,它是一個字符串。

這不是很好,如果我們想要利用 type 屬性上的可區分聯合的話,那麼在 TypeScript 3.4 之前,則需要爲每個 action 聲明一個接口或類型:


 1interface SetCount {
 2  type: 'SET_COUNT';
 3  payload: number;
 4}
 5
 6const setCount = (n: number): SetCount => {
 7  return {
 8    type: 'SET_COUNT',
 9    payload: n,
10  }
11}
12
13const action = setCount(3)
14// action has type SetCount

這確實增加了編寫 Redux action 和 reducers 的負擔,但我們可以通過添加一個 const assertion 來解決這個問題:


 1const setCount = (n: number) => {
 2  return <const>{
 3    type: 'SET_COUNT',
 4    payload: n
 5  }
 6}
 7
 8const action = setCount(3);
 9// action has type
10//  { readonly type: "SET_COUNT"; readonly payload: number };

你會注意到從 setCount 推斷的類型已經在每個屬性中附加了 readonly 修飾符,正如文檔的項目符號所述。

這就是所發生的事情:


1{
2  readonly type: "SET_COUNT";
3  readonly payload: number
4};

action 中的每個字面量都被添加了 readonly 修飾符。

在 redux 中,我們創建了一個接受 action 的聯合,reducer 函數可以通過這種操作來獲得良好的類型安全性。在 TypeScript 3.4 之前,我們會這樣做:


 1interface SetCount {
 2  type: 'SET_COUNT';
 3  payload: number;
 4}
 5
 6interface ResetCount {
 7  type: 'RESET_COUNT';
 8}
 9
10const setCount = (n: number): SetCount => {
11  return {
12    type: 'SET_COUNT',
13    payload: n,
14  }
15}
16
17const resetCount = (): ResetCount => {
18  return {
19    type: 'RESET_COUNT',
20  }
21}
22
23type CountActions = SetCount | ResetCount

我們創建了兩個接口 RESET_COUNT 和 SET_COUNT 來對兩個 resetCount 和 setCount 的返回類型進行歸類。

CountActions 是這兩個接口的聯合。

使用 const assertions,我們可以通過使用 const、 ReturnType 和 typeof 的組合來消除聲明這些接口的需要:


 1const setCount = (n: number) => {
 2  return <const>{
 3    type: 'SET_COUNT',
 4    payload: n
 5  }
 6}
 7
 8const resetCount = () => {
 9  return <const>{
10    type: 'RESET_COUNT'
11  }
12}
13
14type CountActions = ReturnType<typeof setCount> | ReturnType<typeof resetCount>;

我們從 action creator 函數 setCount 和 resetCount 的返回類型中推斷出一個很好的 action 聯合。

數組字面量成爲只讀元組


在 TypeScript 3.4 之前,聲明一個字面量數組將被擴展並且可以修改。

使用 const,我們可以將字面量鎖定爲其顯式值,也不允許修改。

如果我們有一個用於設置小時數組的 redux action 類型,它可能看起來像這樣:


1const action = {
2  type: 'SET_HOURS',
3  payload: [8, 12, 5, 8],
4}
5//  { type: string; payload: number[]; }
6
7action.payload.push(12) // no error

在 TypeScript 3.4 之前,擴展會使上述操作的字面量屬性更加通用,因爲它們是可以修改的。

如果我們將 const 應用於對象字面量,那麼就可以很好地控制所有內容:

1const action = <const>{
 2  type: 'SET_HOURS',
 3  payload: [8, 12, 5, 8]
 4}
 5
 6// {
 7//  readonly type: "SET_HOURS";
 8//  readonly payload: readonly [8, 12, 5, 8];
 9// }
10
11action.payload.push(12);  // error - Property 'push' does not exist on type 'readonly [8, 12, 5, 8]'.

這裏發生的事情恰恰是文檔的要點:

payload 數組確實是 [8,12,5,8] 的“只讀”元組(不過我並沒有從文檔中看到這方面的說明)。

結論


我用以下代碼總結以上所有內容:


1let obj = {
2  x: 10,
3  y: [20, 30],
4  z: {
5    a:
6      {  b: 42 }
7  } 
8} as const;

對應於:


1let obj: {
2  readonly x: 10;
3  readonly y: readonly [20, 30];
4  readonly z: {
5    readonly a: {
6      readonly b: 42;
7    };
8  };
9};

在這裏,我可以推斷出類型,而不是去編寫多餘的樣板類型。這對於 redux 特別有用。

原文:https://blog.logrocket.com/const-assertions-are-the-killer-new-typescript-feature-b73451f35802

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章