iT邦幫忙

2025 iThome 鐵人賽

DAY 2
0

https://ithelp.ithome.com.tw/upload/images/20250831/20118113BLkCodRJyL.png

集合

程式中的型別概念,其實就是數學中的集合概念,而Typescript是架構在Javascrpt之上的型別語言,所以在學習Typescript之前,我們先來複習集合的重要觀念,再以集合的角度來解釋Typescript中的型別概念。

基本概念

集合(Set)的概念:

集合是將一些不重複的物體組成一個整體,這個整體就叫做集合,集合中的物體叫做元素。

集合的表示方法:

  1. 列舉法:將集合中的元素一一列舉出來,元素之間用逗號隔開,並用大括號括起來。
    例如:{1, 2, 3, 4, 5},表示由1, 2, 3, 4, 5這五個元素組成的集合,通常元素個數少時使用列舉法。

  2. 描述法:將集合中的元素的共同特徵描述出來,用大括號括起來,通常元素個數多或無限集合可使用描述用。
    例如:{x|x是正整數},表示所有正整數的集合。

    元素和集合的關係只能是屬於(∈)和不屬於(∉)二者之一,這個關係必須是確定的,不能有模糊空間;集合中的元素不能重複,也沒有順序之分。

理髮師悖論:
有一個理髮師,他聲稱:「我給且只給不自己刮鬍子的人刮鬍子。」
請問:這個理髮師的鬍子該不該由他自己來括?這個理髮師本身是否屬於他自己描述的集合便無法是一個確定的結論。

子集合

如果集合A中的每一個元素也是集合B的元素,則集合A是集合B的子集,記作 A⊂B,讀作A包含於B。
例如: A = {1, 4, 5}, B = {1, 2, 3, 4, 5},則A⊂B。

  • 如果 A⊂B且B⊂A,則A=B。
  • 兩集合的關係不滿足三一律,即有可能A⊂B、B⊂A、A=B三個都不成立。 例如:A = {1, 4, 6}, B = {1, 2, 3, 4, 5}
  • 子集合的關係滿足遞移性,即A⊂B且B⊂C,則A⊂C。

空集合

一個集合中沒有任何元素的集合。空集合用{}表示或∅表示。

集合的運算

設A,B為兩集合。

  1. 交集:兩個集合中共同的元素所組成的集合,記作A ∩ B。
  2. 聯集:兩個集合中所有的元素所組成的集合,記作A ∪ B。
  3. 差集:一個集合中除去另一個集合中的元素所組成的集合,記作A - B。
  4. 全集:一個集合中所有的元素所組成的集合,通常記作U。
  5. 補集:全體集合中除去一個集合中的元素所組成的集合,即A' = U - A。
  6. 冪集:一個集合中所有的子集所組成的集合。

例如:

A = {1, 2, 3, 4}

B = {3, 4, 5, 6, 7}

  1. A ∩ B = {3, 4}

  2. A ∪ B = {1, 2, 3, 4, 5, 6, 7}

  3. A - B = {1, 2},B - A = {6, 7}

  4. A' = {6, 7, 8, 9, 10}

  5. A的冪集 = {∅, {1}, {2}, {3}, {4}, {1, 2}, {1, 3}, {1, 4}, {2, 3}, {2, 4}, {3, 4}, {1, 2, 3}, {1, 2, 4}, {1, 3, 4}, {2, 3, 4}, {1, 2, 3, 4}}共有16個元素。

Typescript型別

Typescript 和 JavaScript

Typescript 是 JavaScript 的超集,也就是所有 JavaScript 的程式碼都可以在 Typescript 中執行。Javascript本身沒有型別的概念也缺代型別檢查的功能,程式容易撰寫但是也容易出錯,於是微軟開發了 Typescript 這個語言,Typescript 是強型別的程式語言,它提供了型別檢查的功能,讓程式碼在撰寫的時候就能夠發現錯誤,這樣可以減少程式碼的錯誤,提高程式碼的品質。

你可能會看到到一些程式碼中會出現 var 這個關鍵字,var 是比較舊的寫法,現在已經不建議使用,接下來的內容我們只會使用 let 和 const 來宣告變數。

Typescript的基本型別

我們先介紹Typescript會使用到的基本型別:
基本型別:

- number:數字型別,包括整數和浮點數所成的集合。
- string:字串型別,包括單引號和雙引號所成的集合。
- boolean:布林型別,包括 true 和 false的集合。
- undefined:未定義型別,表示變數沒有被賦值。
- null:空值型別,表示變數沒有值。
- any: 任意型別,包括所有型別的元素,也就全集。
- never: 等於空集合,表示不可能發生的型別。

我們可以用let或const宣告變數,並且加上型別註解,let宣告的變數可以重新賦值,const宣告的變數不可以重新賦值,例如:

let a: number;
const b: string;
const c: boolean;

這樣宣告的意義是變數a是數字型別的元素,變數b是字串型別的元素,變數c是布林型別的元素。

如果我們宣告變數的時候同時給定初始值,那麼這個初始值必須是符合型別註解,也就是初始值必須是該型別的元素,否則會出現錯誤,never是空集合,所以指定任何初始值都會出現錯誤;相對地,any是全集,所以指定任何初始值都不會出現錯誤,例如:

let a: number = "hello"; // 會出現錯誤
const b: string = 3; // 會出現錯誤
const c: boolean = "true"; // 會出現錯誤
let e: never = 1; // 會出現錯誤
const f: any = 1; // 不會出現錯誤

因為我們宣告變數為any型別並指定任何初始值時,typescript的型別檢查一定不會出錯,而我們就失去了Typescript的型別檢查的意義,所以除非必要,否則不要使用any型別,如果我們在ts。
Typescript還有一種資料型別symbol,在本系列文中將不會使用,我們就不介紹。

如果我們宣告變數的時候沒有型別註解,那麼Typescript會自動推斷型別,例如:

let a = 1;
const b = "hello";
const c = true;

Typescript會自動推斷變數a是數字型別,變數b是字串型別,變數c是布林型別。

除了基本型別外,Typescript還有陣列(Array)、元組(Tuple)和物件(Object)三種複合型別。

number、string和boolean的運算和一般語言差不多,這邊就不介紹了。
string的運算有+,可以將兩個字串變數接在一起,也可以用字串模板(template string)將兩個字串變數嵌在字出模板中。

陣列型別(Array)

Typescript陣列型別(Array type)的概念和Javascript陣列的概念不同,Typescript的陣列是相同型別所形成的集合,陣列中的資料稱為元素。使用Javascript的陣列符號中括號,將資料放入中括號中,資料之間用逗號隔開。陣列中的元素必須是相同的型別,陣列中的元素個數不是固定,例如:

const a: number[] = [1, 2, 3];
const b: string[] = ["hello", "world"];
const c: boolean[] = [true, false];

元組型別(Tuple)

元組型別也是使用Javascript中括號,也是使用Javascript的陣列符號中括號,將資料放入中括號中,資料之間用逗號隔開。不同於Typescript的陣列型別,元組型別可以將不同型別資料放入中括號中,每個索引對應到一特定的型別,其資料的長度卻是固定,例如,我們可以將姓名、年齡和是否學生一同放入元組中成為一個型別,

const person: [string, number, boolean] = ["John", 30, true]

Javascript本身沒有型別的概念,Typescript中的陣列型別和元組型在Javascript都通稱為陣列,實際的應用上,卻是不同的概念,元組型別(Tuple)的使用情境比較接近下面要介紹的物件型別。

物件型別

使用元組型別來複合不同型別資料的缺點是開發者必須記住每個索引對應的型別,當複合的資料多時,記憶不方便。Typescript還提供了另一種鍵值對的資料型別,稱為物件型別。物件型別也是Typescript的一種複合型別,是一種鍵值對的集合,鍵是字串,每一個鍵稱為物件的屬性,值可以是任何型別,將鍵值以{key1: value1, key2: value2, ...}的格式列出。使用物件型別的可讀性比元組更高,例如上面person的變數,我們可以採用下面的方式定義:

const person: {
    name: string;
    age: number;
    isStudent: boolean;
} = {
    name: "John",
    age: 30,
    isStudent: true,
}

鍵除了可以是string,也可以是number和symbol,在這系列中,我們的鍵將只使用string。

型別別名(type alias)

在操作物件和元組型別的時候,為了方便型別的重複使用,我們會用關鍵字type來定義型別別名(type alias),賦予型別別名一個有意義的名稱,例如:

type Person = {
    name: string;
    age: number;
    isStudent: boolean;
}
const john: Person = {
    name: "John",
    age: 30,
    isStudent: true,
}
type Student = [string, number]
const mary: Student = ['mary', 28]

Typescript還提供了介面(Interface),它的寫法如下:

interface Person {
    name: string;
    age: number;
    isStudent: boolean;
}

和type定義物件型別沒有很類似,常常也被使用者交換使用,最大的不同點是interface可以重複宣告,具有擴充性,而type則不行。

有時為了增進程式碼的可讀性,我們也常常將number、string、boolean、never等型別定義成型別別名,這樣定義元組型別時會更具可讀性,例如:

type Age = number;
type Name = string;
type IsStudent = boolean;
type Person = [Name, Age, IsStudent]

雖然有時候會覺得這樣做有點多此一舉,但是這樣做可以讓程式碼更易讀,而且可以避免型別錯誤,至於是否要這樣做,就看個人的習慣了。

關鍵字keyof

keyof可以取得一個物件的key的型別而形成一個union type,例如:

// Person 是一個 object type
type Person = {
  firsName: string;
  lastName: string;
};

type PersonKey = keyof Person; // "firsName" | "lastName"

Indexed Access Types

Indexed Access Types 是 Typescript 的一個特性,它允許我們從一個物件型別中,根據索引值來存取屬性。

type Person = {
    name: string;
    age: number;
    isStudent: boolean;
}
type NameOfPerson = Person["name"]; // string

Index Access Types 也可以用在陣列型別中,它允許我們從一個物件型別中,根據索引值來存取屬性,只是此時的索引值為0,1,2,…。陣列型別的還有一個特別的字串索引值"length",可以得到陣列長度的字面值型別(參照後面字面值型別的說明)。

index signature

當一個物件的key值不是很重要,而我們只在乎key的型別和值的型別時,我們可以使用index signature來描述key和值的型別規範即可。

type Data = {
    [key: string]: string;
}

物件型別的屬性的引用和修改

Javascript物件屬性的引用方法有點記法(Dot notation)和括弧記法 (Bracket notation)兩種,以上面例子為例,我們可以用點記法來引用物件的屬性,例如:

// 使用點記法來引用物件的屬性
console.log(person.name); // John
console.log(person.age); // 30
// 使用括弧記法來引用物件的屬性
console.log(person["name"]); // John
console.log(person["age"]); // 30

console.log是Typescript在控制台中輸出

物件屬性的修改方法也有點記法(Dot notation)和括弧記法 (Bracket notation)兩種,以上面例子為例,我們可以用點記法來修改物件的屬性,例如:

// 使用點記法來修改物件的屬性
person.name = "Mary";
person.age = 25;
// 使用括弧記法來修改物件的屬
person["name"] = "Mary";
person["age"] = 25;

使用括弧記法來引用物件的屬性時,我們可以動態的指定屬性名稱,而點記法只能使用靜態的屬性名稱,例如:

const propertyName = "name";
console.log(person[propertyName]); // John

字面值型別(Literal type)

Typescript 還有一個型別叫做字面值型別(Literal type),這個型別就是單一元素所形成的集合,而這個單一元素必須是number,string和boolean三種型別之一,例如:

type First = 1; // First = {1}
type Hello = "hello"; // Hello = {"hello"}
type True = true; // True = {true}
let a: First = 1;
a = 2; // 2不屬於First這個集合,所以會出現錯誤
const b: Hello = "world"; // "world"不屬於Hello這個集合,所以會出現錯誤
const c:#### 字面值型別(Literal type) True = false; // false不屬於True這個集合,所以會出現錯誤

另外,字元字面值型是字元型別的子集合,所以一個定義為字串型別的字串變數可以被賦值為字元字面值型別,例如:

type World = 'world'
let x: string = "hello";
let y: World = "world";
x = "world"; // 不會出現錯誤
y = x; // 會報錯 Type 'string' is not assignable to type '"world"'.

型態A是型態B的子集合,則宣告型態B的變數可以被賦值給型態A的變數,反之則不行。

型別樣板字串Template Literal Types

Typescript 還有一個型別叫做型別樣板字串(Template Literal Types),這個型別就是字串型別的子集合,這個型別可以接受一個字串字面值型別作為參數,然後產生一個新的型別,例如:

type Greeting = "hello"; // Greeting = {"hello"}
type Greeting2 = `${Greeting} world`; // Greeting2 = {"hello world"}

型別樣板字串的型別配合Union Types可以產生很多有趣的型別,例如:

type HandledEvent = 'change' | 'click' | 'keydown';
type EventHandler = `on${HandledEvent}`; //  "onchange" | "onclick" | "onkeydown"

type Horizontal = 'left' | 'right';
type Vertical = 'top' | 'bottom';
type Position = `${Horizontal}-${Vertical}`; // "left-top" | "left-bottom" | "right-top" | "right-bottom"

屬性修飾符

在Typescript中,我們可以紿予物件型別的屬性加上型別修飾,物件型別的屬性修飾符有兩種:

  • 可選屬性:在屬性名稱後面加上問號,表示這個屬性是可選的,也就是說這個屬性可以存在也可以不存在,例如:
  • 唯讀屬性:在屬性名稱前面加上 readonly,表示這個屬性是唯讀的,也就是說這個屬性只能在物件建立時賦值之後就不能被修改。

可選屬性

在Typescript中,我們可以定義物件的屬性是可選的,也就是說這個屬性可以存在也可以不存在,我們用問號加在屬性名後面來表示可選屬性,此時在初始化物件的時候可以不給定這個屬性,型別檢查也不會出錯,例如:

const person: {
    name: string;
    age: number;
    isStudent: boolean;
    gender?: string;
} = {
    name: "John",
    age: 30,
    isStudent: true,
}

唯讀屬性

在Typescript中,我們可以要求物件的屬性只有在物件建立時能賦值之後就不能被修改,,那麼可以用 readonly 定義唯讀屬性,例如:

const person: {
    readonly id: number;
    name: string;
    age: number;
    isStudent?: boolean;
} = {
    id: 1,
    name: "John",
    age: 30,
}
person.id = 2; // 會出現錯誤
const person = {
   name: "John",
   age: 30,
}
person.name = "Mary"; // 不會出現錯誤

雖然用const 宣告的變數不能被重新賦值,但是const宣告的物件變數時,變數的值其實是物件的參照位址,所以物件的屬性是可以被修改的;陣列也是類似的情形,宣告為const的陣列變數其實是陣列的參照位址,所以陣列的元素是可以被修改的。

物件表示法的省略寫法

當物件的值為一個變數名,而且屬性名稱和的變數名稱相同時,我們可以省略變數名,例如:

const name = "John";
const person = {
    name,
    age: 30,
    isStudent: true,
}

陣列和物件解構

在Typescript中,我們可以將陣列和物件解構,解構運算子為...,解構語法為[...陣列],以下是常見的解構使用用範例:

const numbers = [1, 2, 3, 4, 5];
const [first, second, ...rest] = numbers;
console.log(first); // 1
console.log(second); // 2
console.log(rest); // [3, 4, 5]

物件解構的語法為{...物件},以下是常見的解構使用用範例:

const person = {
    name: "John",
    age: 30,
    isStudent: true,
}
    const { name, age, ...rest } = person;
    console.log(name); // John
    console.log(age); // 30
    console.log(rest); // { isStudent: true }

暨然Typescript的每一個型別就是一個集合,我們當然可以利用集合的運算來進行型別的運算。以下下是一些常見的型別運算:

  1. 聯集(|):
type NumberOrString = number | string;
const a: NumberOrString = 1;
const b: NumberOrString = "hello";
const c: NumberOrString = true; // 會出現錯誤

我們可以聯集搭配字面值型別來定義一些有限元素的狀態集合,例如:

type Status = "pending" | "inProgress" | "completed"; //有三個元素的集合
let status = "pending"; // OK
status = "completed"; // OK
status = "inProgress"; // OK
status = "error"; // 會出現錯誤

Indexed Access Types對Union具有分配律,

如果Person是物件

Person['name' | 'age' ] = Person['name'] | Person['age']

如果Person是元組(Tuple)或陣列(Array)

Person[ 0 | 1 ] = Person[0] | Person[1]

思考下面程式中Range4的型別:

type Numbers = [1, 2, 3, 4]
type Range4 = Numbers[number] // 1 | 2 | 3 | 4
  1. 交集(&):

    如果兩個物件型別進行交集,那麼交集的結果必須同時擁有兩個物件型別的屬性,因此交集得到的新物件型別的屬性反而是原來兩個物件型別屬性的聯集;而如果兩個物件型別進行聯集,那麼聯集的結果只要兩個物件型別的屬性,因此聯集得到的新物件型別的屬性反而是原來兩個物件型別屬性的交集。這點容易混淆,我們用例子來說明:

type BusinessPartner = {
  name: string;
  credit: number;
}

type Identity = {
  id: number;
  name: string;
}

type Employee = Identity & BusinessPartner;
/**
 * Employee 是 Identity 和 BusinessPartner 的交集,等同於
 * type Employee = {
 *  id: number;
 *  name: string;
 *  credit: number;
 * }
 */
export const olddunk: Employee = {
  id: 123,
  name: 'Olddunk',
  credit: 321
}

type EmployOrBusinessPartner = Employee | BusinessPartner;
/**
 * EmployOrBusinessPartner 是 Employee 和 BusinessPartner 的聯集,等同於
 * EmployOrBusinessPartner = {
 *  name: string;
 * }
 */ 
 export const sean: EmployOrBusinessPartner = {
    name: 'Sean',
 }

undefined 和 null

在Typescript中,undefined通常出現在開發者未注意資料可能沒有被賦值,因此得到的值便是undefined,而由undefined單一值所成的資料型態可以被記作void,也就是{undefined}集合,常出現的場景有以下幾種:

  1. 變數沒有被賦值時,預設值為undefined。
  2. 函式沒有返回值時,預設返回值為undefined。
  3. 函式的參數沒有被傳遞時,預設值為undefined。
  4. 使用解構賦值時,如果屬性不存在,則值為undefined。
  5. 如果陣列元素不存在,則值為undefined。
  6. 如果物件屬性不存在,則值為undefined。

null是一個空值,通常是開發者主動賦值給物件變數,作為物件變數的初始值。

函數型別

今日小結

程式的世界是有限的,而是數學的世界卻是無限,因此集合論中有些概念,Typescript無法完美實作,因此無法完全用集合的概念來套用Typescript的型別。例如有些時候,我們覺得包含的概念成立,但是Typescript的extends卻不對,要稍稍調整。

用一天的篇幅介紹Typescript型別的基本用法,份量有些重,所以無法也不可能將Typescript完整的說明,主要是將這個系列會用到的概念打包輸出,如果有不足之處,未來會適時補充。即便已經是最小輸出,但是內容還是不少,未曾學過Typescript的讀者可能要花不少時間消化,今天就到此休息了!


上一篇
Day 01. 數學與程式設計的交會
系列文
數學老師學函數式程式設計 - 以fp-ts啟航2
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言