繼上一篇 – 有趣的 TypeScript 型別用法與機制,這次閱讀到 Gabriel Vergnaud 的 Type-Level TypeScript 這系列文章的第 1 到 4 章,屬於免費閱讀的章節,讓我重新認識了 TypeScript (以下簡稱 TS) 的型別系統;該系列文章中有提供即時的程式碼編輯區塊,每一章最後都有小測驗,閱讀與學習的體驗都很棒,非常值得一看。
這篇文章的內容是我個人針對這系列做的一個學習筆記,在寫這篇文章時也意外發現其實 Gabriel 提到的內容,TS 官網上都有條目寫到;過去只是讀過,從沒理解過😅。
Levels of Language
貫穿該系列文章的第一個重點;language of values (value-level) 和 language of types (type-level),JavaScript (以下簡稱 JS) 因為(還)沒有型別的語法所以 100% 是 value-level 語言。
還沒有,但正在提案中的型別註釋– tc39/proposal-type-annotations: ECMAScript proposal for type syntax that is erased - Stage 1
如果對 TS 實作不陌生,有個錯誤就是:'Somthing' only refers to a type, but is being used as a value here
– TS Playground;正是這個 value 和 type 的概念。於是乎,TS 不只是給 JS 寄住的外殼,而是用來撰寫型別,type-level 程式語言。
Generics– 泛型
上一篇提過 JS/TS 內建的型別可以透過給 type parameter 進 <type>
來產生型別,像是– TS Playground
type ObjType = Record<string, number>;
const beverageOrder = new Map<string, number>();
過去我一直以為泛型的應用一定要給 type parameter 做型別宣告,像是這樣– TS Playground:
type LogType<A, B> = (P: A, Cb: B) => B;
// A higher order function
// log out the types of a function's input(parameter) and its output
function logType(param: unknown, cb: (val: unknown) => unknown) {
const result = cb(param);
console.log(`Input Type: ${typeof param}\nOutput Type = ${typeof result}`)
return result;
}
const res1 = logType(
1234,
(val) => JSON.stringify(val)
) as LogType<number, string>;
const res2 = logType(
{id: 1234, name: "foo"},
(val) => Object.keys(val as Object).map(item => item.toUpperCase())
) as LogType<object, Array<string>>;
console.log("res1", res1); // "1234"
console.log("res2", res2); // ["ID", "NAME"]
又臭又長,不但需要先訂好輸入與輸出的 type parameters,還要靠 as
把型別給上去,甚至在 res2
的 Object.keys()
中也需要給 val
參數用 as
加定義,不僅難以複用,還要寫太多程式碼了。
但重新認識泛型後,才知道可以這樣應用– TS Playground:
// A higher order function
// log out the types of a function's input(parameter) and its output
function logType<Input, Output>(param: Input, cb: (val: Input) => Output): Output {
const result = cb(param);
console.log(`Input Type: ${typeof param}\nOutput Type = ${typeof result}`)
return result;
}
const res1 = logType(
1234,
(val) => JSON.stringify(val)
);
const res2 = logType(
{id: 1234, name: "foo"},
(val) => Object.keys(val).map(item => item.toUpperCase())
);
console.log("res1", res1); // "1234"
console.log("res2", res2); // ["ID", "NAME"]
可以看到 logType
使用了泛型宣告 Input
和 Output
,但在指派給 res1
使用時卻不必給 type parameters,而是反過來,由 TS 從給進去的參數推斷 Input
和 Output
該有的型別是什麼。
Data Structures
在原系列文章的第二章– Types are just data - Data Structures 該節中提到:
In our type-level world, we have four built-in data structures at our disposal:
四個 type-level 的資料結構為:
type ObjectType: {key1: number, key2: string};
// 有限的鍵(key)數配對不同型別的值(value)
type RecordType: {[key: string]: number};
// 類似 object type,但為不限數量的鍵配對特定型別的值
type TupleType: [string, number];
// 有限長度的資料集,個別資料的型別可能不同
type ArrayType: number[];
// 未知長度的資料集,但與 record type 相像,所有資料有特定的型別
這四個資料結構分別在該系列文章的,
- 第三章 Objects & Records
- 第四章 Arrays & Tuples
深入的討論四種型別資料結構的特性,如何 merge 及 extract 當中的屬性…等。
Types are Sets
在原系列文章的第二章– Types are just data - Types are Sets 提到,
An interesting feature of TypeScript is that a value can belong to more than one type.
這是第二個貫穿系列文章的重點– assignability;
下面這個錯誤訊息在 TypeScript 常見到,
“Type A is not assignable to Type B”
放個方向想,當"A is assignable to B",A 即是 B 的 subset,B 的型別範圍涵蓋 A 的型別範圍,所以可以把 A 放進 B 裡,Gabriel (該文章作者) 稱之為 assignability。
Objects and Records
文章第三章中提到,關於物件型別,在 TS 中跟 JS 不同的地方:
- 在 type-level,要讀物件的屬性不能用 dot (
obj.prop
) 只能用 square brackets notation:obj["prop"]
namespace
中export
出來的物件倒是可以用 dot 讀取– TS Playground - Value-level (JS) 的 Logical AND (
&&
) / OR (||
) operator 和 type-level (TS) 中 union (|
) / intersection (&
) 運作機制是不同的– TS Playground - JS 中物件屬性的 merging 會透過 spread operator (
...
) 或物件方法,如Object.assign()
達成,在 type-level 中則是用&
、extends
,或implements
等達成,可以參考 stackoverflow 上的這則回答。
題外話:Gabriel 有提到由於在
type
使用&
來 merge 屬性 TS 會持續遍歷過當中全部的型別來產生最終的結果,如果不是需要泛型,interface
會是效能上比較好的選擇,詳細的說明可以參考這篇文章的章節。
Arrays and Tuples
文章第四章中提到,關於陣列型別的應用和特色:
-
想要取到陣列中的值的「型別」可以用
arr[number]
-
使用
keyof
可能不是理想的選擇,試想在 JS 中下面這段程式碼會印出什麼?在 type-level 也會拿到這些內建的屬性鍵:const arr = ['foo', 'bar', 'map'] console.log(arr['map'])