[TypeScript] The Very Basics for TS

Type Inference & Annotation、Primitive TypesObject Types、Function Type、Enums Type、Union & Intersection Type、typeof、keyof

Hannah Lin
Hannah Lin
17 min readApr 18, 2021

--

(更新於 2022/3/24)

TypeScript 系列文

1. The Very Basics for TS
2. Generics 的使用情境
3. The `extends` keyword
4
. GenericsMapped Types
5. 用生活例子圖解 Utility Types
6
. 優雅的在 React 中使用 TS
7. 用 ts-migrate 仙女棒讓 JS 專案瞬間 migrate 成 TS

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 Typesnullundefined 會被推斷為 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 使用起來很單純,包含 numberstringbooleanundefinednullsymbol 等。

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 另一個好處就是他的可讀性了,不然誰知道 3264 代表的是什麼

補充: 我該用 Type 還是 Interface ?

會發現定義型別時有人用 Type 有人用 Interface,那到底要用哪個比較好呢? 其實就是看團隊決定,自己團隊是除了需要指定型別時才會用 Type,其他一率使用 Interface

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 只適用於 interfacetype 是沒辦法使用的

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 必須有 eqmatch 這兩個 key,而且 value 的型別都要為 string。若是要把 myFilter 值限制在 "eq" | "match" ,可以用 keyof typeof

--

--