前情提要
YouTube 頻道Visual Studio Code
前陣子推出了由大神 Matt 所介紹有關 TypeScript 奇技淫巧影片
相當燒腦,特別作此記錄。
難度:★★☆☆☆
這題需要有一些 React 的基礎概念,
首先我們有一些 React Component(以下簡稱 RC),
並且用 interface 建立相對應 props。
問題是如果要讓我們的 RC 更泛用應該怎麼作,
在這個例子中,我們建立了一個 Table RC(以下簡稱 Table) ,並且在 props 中傳入一些參數,
這個參數的型別,直接與 Table 產生了耦合,我們可用泛型(Generic)處理。
Before
1 2 3 4 5 6 7 8
| interface TableProps { items: { id: string }[]; renderItem: (item: { id: string }) => React.ReactNode; }
export const Table = (props: TableProps) => { return null; };
|
After
1 2 3 4 5 6 7 8
| interface TableProps<T> { items: T[]; renderItem: (item: T) => React.ReactNode; }
export const Table = <T,>(props: TableProps<T>) => { return null; };
|
這裡是很簡單的泛型觀念,如果有寫過具備泛型的語言(ex:C#)應該不難理解,
另外需要注意的一個有關 React 的小小 Tricky
注意到 <T,>
這裡有一個小小的逗號,其實是不必要的,
但是在 React 的開發環境之中,角括號<>
會被視為未封閉的 RC;
封閉的例子,ex:<myComponent></myComponent>
或是 <myComponent />
未了必免編輯器的警告,需要加上一個逗號,
另外這裡在寫 React Component 時,使用了 arrow function,
我們也可以換成一般的 function 寫法,這種寫法就可以省略 <T,>
這樣 tricky 的寫法
1 2 3
| export const Table = function <T>(props: TableProps<T>) { return null; };
|
泛型不難理解,但是考量到需要具備的 React 知識,難度我給他兩顆星
難度:★★★☆☆
這題要動態的取出物件深層的屬性的型別,
具體好處是可以讓編輯型別檢查
看一下影片中的範例 getDeepValue
是一個函數,可以用來取物件深層屬性的值
這裡我們看到 obj:any
Matt 說道可以視作一個壞味道(不知道他對 unknown 的看法)
1 2 3
| export const getDeepValue = (obj: any, firstKey: string, secondKey: string) => { return obj[firstKey][secondKey]; };
|
看一下原始範例如下
1 2 3 4 5 6 7 8 9 10
| const obj = { foo: { a: true, b: 2, }, bar: { c: "12", d: 18, }, };
|
我的作法
如果比較直觀的寫法可以考慮寫個 ObjectType,
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
|
const obj: ObjType = { foo: { a: true, b: 2, }, bar: { c: "12", d: 18, }, };
type FirstKeyType = "foo" | "bar"; type SecondKeyType = "a" | "b" | "c" | "d"; type ObjType = { [key in FirstKeyType]: { a: boolean; b: number } | { c: string; d: number }; }; export const getDeepValue = (obj: ObjType
|
註:這裡有用到 index-signatures 的概念
看看官方文件的說法:
1 2 3
| Sometimes you don’t know all the names of a type’s properties ahead of time, but you do know the shape of the values.
In those cases you can use an index signature to describe the types of possible values
|
1
| const value = getDeepValue(obj, "foo", "a");
|
Matt 在影片中的作法
1 2 3 4 5 6 7 8 9 10 11
| export const getDeepValue = < TObj, TFirstKey extends keyof TObj, TSecondKey extends keyof TObj[TFirstKey] >( obj: TObj, firstKey: TFirstKey, secondKey: TSecondKey ) => { return obj[firstKey][secondKey]; };
|
keyof 關鍵字可以直接參考官方文件
比較兩者的作法
我們加上了 Type,就是要為程式碼加上保護
我跟 Matt 的作法,下面的程式碼都會提出警告
1
| var error_value = getDeepValue(obj, "error", "wrong");
|
但是無法避免以下的錯誤
1
| var error_value = getDeepValue(obj, "foo", "c");
|
obj.foo
是不存在 c
屬性,這樣我們就需要動態判斷型別
看看 Matt 使用 keyof
作法。
1 2 3 4 5 6 7
| export const getDeepValue = <TObj, TFirstKey extends keyof TObj>( obj: TObj, firstKey: TFirstKey, secondKey: keyof TObj[TFirstKey] ) => { return obj[firstKey][secondKey]; };
|
這裡有點不可思議,首先是 ts 的動態型別推導,
在呼叫方法 getDeepValue 的第 1 個參數會動態推導出 TObj 的型別
大概等價以下的型別
1 2 3 4 5 6 7 8 9 10
| type TObj = { foo: { a: boolean; b: number; }; bar: { c: string; d: number; }; };
|
keyof TObj
的值會是 foo | bar
而 TFirstKey 的型別,在第 2 個參數傳入之時決定是 foo
或 bar
,
不是上述兩種的參數傳入時,編輯會拋出錯誤警告,非常好用。
下一個燒腦的部份,第 3 個參數 secondKey: keyof TObj[TFirstKey]
,
透過類似陣列取值的手法,我們可以在傳入第 2 個參數時決定第 3 個參數的值,並且讓編輯器提供保護。
例如,第 2 個參數為 foo
時,第 3 個參數會限定只能傳入 "a"|"b"
這裡需要思考一下,並不難理解,所以給 3 顆星。
如果能應用得好的話,對於開發 ts Library 會相當有幫助
不過我覺得自已無法用得很得心應手,比如說,如果物件深度不固定,應該如處理?
又或者如果不用 keyof
的技巧,對一個已知的物件,我能不能作到類似的效果(編輯器動態檢查)?
難度:★★★☆☆
這其實是 TypeScript 一個基本的概念與技巧,
可以在官方文件看到更多資訊
一個標準的寫法如下
1
| SomeType extends OtherType ? TrueType : FalseType;
|
記得這裡最終只會回傳 TrueType 或是 FalseType,而 SomeType 只是作為條件檢查。
當然也可以作多重的條件檢查
1 2 3 4 5
| SomeType extends OtherType ? TrueType : SomeType extends OtherConditionType? OtherThing : FalseType;
|
35:20 更進一步
我們看一下怎麼應用 Condition Type 這個技巧?
參考error-messages-in-ts這個教案
在 deepEqualCompare 這個方法之中,陣列比較需要例外處理,
因為陣列比較永遠會回傳 false
原本的作法,會在程式的 runtime 作型別檢查並拋出例外
1 2 3 4 5 6 7
| export const deepEqualCompare = <Arg>(a: Arg, b: Arg): boolean => { if (Array.isArray(a) || Array.isArray(b)) { throw new Error("You cannot compare two arrays using deepEqualCompare"); } return a === b; };
|
而透過 condition type,可以讓編輯器在開發時直接檢查,看起來更加的優雅
1 2 3 4 5 6
| export const deepEqualCompare = <Arg>( a: Arg extends any[] ? "not allow array" : Arg, b: Arg extends any[] ? "not allow array" : Arg ): boolean => { return a === b; };
|
這應該算是 TypeScript 的基本工,也有在官方文件上有很清楚的講解,
對我來說,第一次看到仍然相當驚訝,我給他三顆星
另外,我有兩個延申問題,
- 程式碼中有 any 算不算壞味道? 我是不是應該用
unknown[]
取代 any[]
Arg extends any[] ? "not allow array" : Arg
明顯出現了重複,我有沒有什麼技巧可以消除這樣的重複?
其它
整個影片還有許多燒腦的部分,
Q&A 或是最後的挑戰都有相當多有趣的東西的可挖掘,
有機會我再補上。
參考
(fin)