[TypeScript] The Very Basics for TS
Type Inference & Annotation、Primitive Types、Object Types、Function Type、Enums Type、Union & Intersection Type、typeof、keyof
(更新於 2022/3/24)
TypeScript 系列文
1. The Very Basics for TS
2. Generics 的使用情境
3. The `extends` keyword
4. Generics — Mapped Types
5. 用生活例子圖解 Utility Types
6. 優雅的在 React 中使用 TS
7. 用 ts-migrate 仙女棒讓 JS 專案瞬間 migrate 成 TS
🔖 文章索引0. Type Inference & Annotation 型別推論與註記
1. Primitive type 原始型別
2. Object type 物件型別
3. Function Type
4. Enums Type 補充. 我該用 Type 還是 Interface5. Union Type
6. Intersection TypesUtility Type
5. keyof
6. typeof
JS 是弱型別(動態型別),而公司專案之前都是用 propTypes (Type checking) 跟 JSDoc (documentation generator for JavaScript)去補足弱型別的不足。
團隊最近想導入 TypeScript (之後簡稱 TS) 取代以上兩種工具,雖然對於要不要使用 TS 網路上其實很兩極,不過優缺點就先不列入此篇文章 (延伸閱讀: Adopting TypeScript Will Make You Suffer)。
寫了一系列自己消化過的 TS 文章來筆記所學,也助日後查找容易。
0. Type Annotation & Inference
除了自己定義型別外 (Type Annotation),很多時候 TS 也會貼心的自動推斷型別 (Type Inference),讓你省寫好幾行 code ! 所以基本上撰寫 TS 時只需要定義那些無法被 TS 推論的型別就好。
Type Inference 型別推斷
TS 有 Inference 會先嘗試幫你推測型別,若推測不出來會用 any
型別取代
const characters = 'snow white'
// TS 會貼心幫你轉成
const characters: string = 'snow white'
所以像下面這些都不需要再另外加型別,不過要注意 Nullable Types 像 null
跟 undefined
會被推斷為 any
Type Annotation 明確指定型別
在 TS 無法自動推斷型別時就需要加型別
- 初始化時: 變數先初始化,待之後才加值
let apples: string; // 初始化時需要加
// 若沒加 TS 就會推測為 any
let apples; // 沒加 type
apples = 5; // typescript 會顯示 let apples: any
- 無法推斷型別時:
groceryLists
裡的參數 TS 推測不出來所以需要自己加型別
type TGroceryLists = {
items: array;
title: name;
done: boolean;
}
const groceryLists = ({items, title, done}: TGroceryLists) => {
// do something here
}
以下就是一般寫專案時 Type Annotation, Type Inference 兩種都會搭配使用
function calculateBonus(salary: number, performanceRating: number) { // Type Annotation
let bonusPercentage: number; // Type Annotation
if (performanceRating > 8) {
// 以下略
}
// Type Inference,因為 number 跟 number 做運算結果一定是 number
const totalBonus = salary * bonusPercentage;
return totalBonus;
}
不同指定 type 的方法
- 註記在變數上,也是最常見的方式
// string
const name: string = fn('hannah')
// array
const lists: string[]
- using
<>
angle brackets
const name = <string>fn('hannah')
// array
const lists: Array<string>
// set
new Set<string>()
// map
new Map<string, number>();
async function getGreeting(): Promise<string> {
return "Hello World!";
}
- using
as
keyword: 常用於未知的值
const name = fn('hannah') as string
// TS 無法推斷 fn('hannah') 是什麼型別
const appDiv = document.getElementsByTagName('div').item(0) as HTMLDivElement;
// TS 無法推斷 document.getElementsByTagName('div').item(0) 是什麼型別
1. Primitive type
primitive type 使用起來很單純,包含 number
、 string
、 boolean
、undefined
、null
、symbol
等。
const name = "Hannah"; // 這邊會自動推斷為 string 所以不需要額外註記型別
type TPrimitive = {
a: number,
b: string:
c: boolean
}
2 Object type
object type 使用情境稍微複雜一些
type objProps = {
a: object; // Useful as a placeholder.
b: {}; // Can have any properties and values.
c: {
id: string;
title: string;
};
d: {
id: string;
title: string;
}[], // An array of objects of a certain shape.
arr: string[] // 或是 Array[string] 也可以
};
{}
允許裡面有任何型態的 key 跟 value,是比較不嚴謹的寫法{ id: string; title: string; }
object 裡面有兩個 properties,id 跟 title 值都是字串,這也是比較建議的寫法
自定義型別
當然你可以獨立拉出一個自定義型別再進行組裝
type Item = {
id: string;
title: string;
}
type objProps = {
c: Item;
d: Item[];
};
若你真的不知道 key 是什麼,也可以這樣寫
type Dictionary = {
[key: any]: any; // 不建議
[key: string]: string; // 建議,key 是 number 型別而值是 string
};
// 以下也可以
type Dictionary = Record<"a"| "b", string>
let dictionary: Dictionary = { a: "A", b: "B"}
關於 key/value pair 博大精深,我會在系列文三 Mapped Types 詳細解釋
3 Function Type
type FunctionProps = {
// Does not take any arguments. Does not return anything.
onHover: () => void;
// Takes a number. Returns nothing (e.g. undefined).
onChange: (id: number) => void;
// Takes an event that is based on clicking on a button.
// Returns nothing.
onClick(event: React.MouseEvent<HTMLButtonElement>): void;
};
// standalone way
const add = (a: number, b: number = 3, c?: number): number => {
return a + b;
};
add(1, 2);
onHover:() => void
,=>
後面代表函式回傳的型別,這邊的void
代表沒有回傳add
function 其實也可以把回傳的型別:number
拿掉因為 TS 會很聰明知道 number + number 一定是 number 啊- optional property
c
一定要在最後面不是會報錯
Function 要定義 type 的頻率非常高,因為 TS 對於函式參數推論結果都是 any
要解決這個 type error 通常會用以下三種方法
4 Enums Type
用來定義一組不會改變的常數 (Static value),例如訂單狀態 (已下單、運送中、已到達)、使用者的 role (Admin、User、Author) 這種不會改變的 value 就很適合放在 Enum 裡
enum Role {
STANDARD = 32,
CUSTOM = 0,
BASIC = 16,
ADMIN = 64
}
interface IUser {
name: string,
role: Role
}
let user: IUser = {
name: 'Hannah',
role: Role.ADMIN // (enum) Role.ADMIN = 64
}
使用 enum 另一個好處就是他的可讀性了,不然誰知道 32
、64
代表的是什麼
補充: 我該用 Type 還是 Interface ?
會發現定義型別時有人用 Type
有人用 Interface
,那到底要用哪個比較好呢? 其實就是看團隊決定,自己團隊是除了需要指定型別時才會用 Type,其他一率使用 Interface
- Interface: commonly used for defining the shape of objects and classes.
- Type: define a type of function or a type alias, types are cool too.
網路上很多文章都說使用哪個都可以,但其實他們還是有些許不同
1. type 可以定義任何類型型別 ; interface 只能定義 object type
type Tname = string;
interface IName = string;
// error, 'string' only refers to a type
type TFruits = 'banana' | 'orange';
type TCondition = 'banana' extends string ? true : false;
若只是定義 Objects / Functions 參數的話,兩者的確都可行
type TName = string;
interface IContact {
name: TName;
}
type TContact = {
name: TName;
}
2. interface 可以 extends ; type 不行
interface Name {
name: string;
}
interface User extends Name {
age: number;
}
const user: User = {age: 11, name: 'Hannah'}
在實際專案中,使用 extends
繼承是相當常見寫法。雖然 type 也可以使用 & 來模擬繼承但實際使用起來卻不如 interface 的 extends
好用
type Name = {
name: string;
}
type User = Name & { age: number }
const user: User = {age: 11, name: 'Hannah'}
3. type 后面有 = ; interface 没有
interface IContact {}
type TContact = {}
4. 多個相同 interface 可以自動合併,但相同情況 type 會報錯
// Declaration merging
// interface
interface Point { name: string; }
interface Point { age: number; }
const harry: Point = {
name: 'Harry',
age: 41
}
// type
type Point = { name: string; }
type Point = { age: number; }
// Error: Duplicate identifier 'Client'
Interfaces vs Types in TypeScript 此篇很詳細去比較兩者不同,有興趣可以看。
5 Union Type 聯集
要特別注意的是物件型別跟一般型別使用方法剛好相反。例如 Union type 一般型別是用 |
而物件是用 &
/* 一般型別 */
type A = "a" | "b" ;
type B = "b" | "c" ;
// Inferred Type: "a" | "b" | "c"
type Union = A | B;
以上 a 、 b 可以想成一般型別如 boolean、number、string、null、undefined 以及 Symbol
物件聯集自己蠻常用的,尤其在有一些外部 common 要引入時。有兩種方法可以達成物件聯集,自己比較常用 extends
,不過要特別注意的是 extends
只適用於 interface
,type
是沒辦法使用的
6 Intersection Types
再次強調 object type 交集跟聯集與一般型態相反,這邊蠻容易搞混的
小練習
UI 接收的 data 格式不止一種,請寫出適合 mockData
的 type filterListProps
type filterDataProps = {
filterName: string;
operator: string;
};
type transformfilterDataProps = {
or?: filterDataProps[];
};
const mockData1: filterListProps = [
{ filterName: 'is_licensed', operator: 'eq'}
]
const mockData2: filterListProps = [
{ or: [
{ filterName: 'last_seen_time', operator: 'lt', value: '-P90D'},
{ filterName: 'last_seen_time', operator: 'nexists' }
]}
]
type filterListProps = ??
type filterListProps = filterDataProps[] | transformfilterDataProps[]
filterDataProps[]
, transformfilterDataProps[]
兩種格式擇一符合就好
那若 mockData
為以下呢?
const mockData: filterListProps = [
{ filterName: 'is_licensed', operator: 'eq'},
{ or: [
{ filterName: 'last_seen_time', operator: 'lt', value: '-P90D'},
{ filterName: 'last_seen_time', operator: 'nexists' }
]}
]
就會發現出現 error,因為他是擇一,若兩種格式同事存在就不符合了,這時改成以下就搞定啦
type filterListProps = Array<filterDataProps | transformfilterDataProps>
7. keyof
keyof
,取出 object type 裡全部的 key,轉換成 union type
若想要抓 key 對應的 value 呢? 可以使用 keyof
搭配 Indexed Access Type 做到,Indexed Access Type 很簡單就是取出 key 裡面的型別
type Obj = {
a: "A";
b: "B";
c: number;
};
// Inferred Type: "A"
type IndexedAccessA = Obj["a"]
// Inferred Type: "B"
type IndexedAccessB = Obj["b"]
// Inferred Type: number
type IndexedAccessC = Obj["c"]
Indexed Access Type 搭配 keyof
就可以撈出所有 Object type 裡的型別了
type Obj = {
a: "A";
b: "B";
c: number;
};
Indexed Access Type
// Inferred Type: "A"
type IndexedAccess = Obj["a"]
// Inferred Type: number | "A" | "B"
type Values = Obj[keyof Obj];
type Obj = {
0: "a";
1: "b";
prop0: "c";
prop1: "d";
};
// Inferred Type: "c"
type Result0 = Obj["prop0"];
// Inferred Type: "a" | "b"
type Result1 = Obj[0 | 1];
// Inferred Type: "c" | "d"
type Result2 = Obj["prop0" | "prop1"];
8. typeof
可以把一般變數宣告變成 type
myFilter
必須有 eq
、match
這兩個 key,而且 value 的型別都要為 string
。若是要把 myFilter
值限制在 "eq" | "match"
,可以用 keyof typeof