跳至主要内容

[typescript] TypeScript 基礎 (使用 Vite 建立環境)

說明

要快速建立 TypeScript 的專案,可以使用 Vite,在 Terminal 輸入以下的指令即可:

npm create vite@latest 你的專案名稱 -- --template vanilla-ts

接著切換到你的專案目錄,執行以下指令,就把基本的環境建置好了:

npm install

將環境 Run 起來:

npm run dev

Union

要讓一個變數擁有多種的型別,可以使用 Union

src/main.ts
let userId: string | number = ""; // userId 只能是 string 或 number 型別userId = true; //Type 'boolean' is not assignable to type 'string | number'.

Tuple

如果要嚴謹的定義陣列內的變數型別,則可以使用 Tuple

src/main.ts
let mixed: [string, number, boolean] = ["Wei", 123, true];mixed[0] = 123; // Type 'number' is not assignable to type 'string'.

Objects & type

定義 Object 內的 Type 可以先創建一個樣板,只要使用 type 關鍵字即可,其他變數要使用該樣板的話必須遵循裡面定義的 Type:

src/main.ts
type Person = {  name: string;  age: number;  friends: (string | number)[];};let Wei: Person = {  name: "Wei",  age: 25,  friends: [1, 2, 3],};let Yun: Person = {  name: "Wei",  age: "26", // Type 'string' is not assignable to type 'number'.  friends: [1, 2, 3],};

另外,如果使用了 Person 當作樣板,變數也都需要將 Person 有的屬性定義出來,不然同樣會發生錯誤:

src/main.ts
type Person = {  name: string;  age: number;  friends: (string | number)[];};// Property 'friends' is missing in type '{ name: string; age: number; }' but required in type 'Person'.let Wei: Person = {  name: "Wei",  age: 25,};

這樣寫是不行的,因為 Person 內沒有 email 這個屬性:

src/main.ts
type Person = {  name: string;  age: number;  friends: (string | number)[];};let Wei: Person = {  name: "Wei",  age: 25,  friends: [1, 2, 3],  email: "test@gmail.com", // { email: string; }' is not assignable to type 'Person'};Wei.email = "test@gmail.com"; // Property 'email' does not exist on type 'Person'.

但我們可以讓 Person 內的屬性變為可選的,只要在屬性名稱後加上 ? 即可:

src/main.ts
type Person = {  name: string;  age: number;  friends: (string | number)[];  email?: string;};let Wei: Person = {  name: "Wei",  age: 25,  friends: [1, 2, 3],};

Interface & Extends

假設我們有兩種型別需要定義,PersonEmployee,對某些物件來說可能只會用到 Person 內定義的屬性,並不會使用到 Employee 內定義的屬性:

src/main.ts
type Person = {  personName: string;  age: number;  friends: (string | number)[];  email?: string;};type Employee = {  employeeName: string;  salary: number;};

這時候如果用 type 來實作的話,可以寫成這樣:

src/main.ts
type Person = {  personName: string;  age: number;  friends: (string | number)[];  email?: string;};type Employee = {  employeeName: string;  salary: number;};type PersonEmployee = Person & Employee {let Wei: PersonEmployee = {  personName: "Wei",  age: 25,  friends: [1, 2, 3],  employeeName: "front-end developer",  salary: 50000,};

但問題出現了,如果我們想要讓 PersonEmployee 也有自己的屬性的話該怎麼辦?

答案是我們可以使用 interface 取代 type,並用 extends 延伸其他先前定義的屬性:

src/main.ts
interface Person = {  personName: string;  age: number;  friends: (string | number)[];  email?: string;};interface Employee = {  employeeName: string;  salary: number;};interface PersonEmployee extends Person, Employee {  bossName?: string;}let Wei: PersonEmployee = {  personName: "Wei",  age: 25,  friends: [1, 2, 3],  employeeName: "front-end developer",  salary: 50000,  bossName: "Yun",};

所以可以這樣說, type 較屬於靜態定義,而 interface 則可以搭配 extends 來達到動態定義。

Type Aliases

如果有重複的型別定義,可以使用 type 將型別儲存成一個樣板:

src/main.ts
type stringOrNumber = string | number;type stringOrNumberArray = (string | number)[];type Person = {  id: stringOrNumber;  friends: stringOrNumberArray;};

Literal Types

我們也可以指定變數能填寫的值:

src/main.ts
let username: "Wei" | "Yun" | "John";//Type '"Alex"' is not assignable to type '"Wei" | "Yun" | "John"'.username = "Alex"; //

Function

如果 Function 需要接收外部參數,則需要定義參數的型別:

src/main.ts
//Parameter 'a' implicitly has an 'any' type.//Parameter 'b' implicitly has an 'any' type.const add = (a, b) => {  return a + b;};

需更改成這樣:

src/main.ts
const add = (a: number, b: number) => {  return a + b;};

也可以指定 Function 的回傳值型別:

src/main.ts
const add = (a: number, b: number): number => {  return a + b;};

如果沒有任何回傳值的話,可以寫成 void

src/main.ts
const log = (message: any): void => {  console.log(message);};

也可以更改成 Type Aliases 的方式:

src/main.ts
type addFunction = (a: number, b: number) => number;const add: addFunction = (a, b) => {  return a + b;};

interface 來寫的話,則寫成以下:

src/main.ts
interface addFunction {  (a: number, b: number): number;}const add: addFunction = (a, b) => {  return a + b;};

Function 接收的外部參數,可以給予預設值:

src/main.ts
const add = (a: number = 10, b: number) => {  return a + b;};add(undefined, 10); // 20add(15, 10); // 25

我們可以使用 Rest Parameters 來將傳遞進 Function 的參數一併處理:

src/main.ts
const sum = (...nums: number[]) => {  return nums.reduce((acc, cur) => acc + cur, 0);};sum(1, 2, 3, 4); // 10

如果有一個不會有回傳值的 Function,則 TypeScript 會自動將它定義為 never,像是 無限迴圈錯誤處理

另外要特別提一點,never 是所有型別的子型別(Bottom Type)。

src/main.ts
const infiniteLoop = () => {  let i: number = 0;  while (true) {    i++;  }};const errorHandler = (errorMessage: any) => {  throw new Error(errorMessage);};

Image

Image

never 型別較常用在錯誤處理的方面,像是 Type Guard,如果傳入 string 或 number 以外的型別,TypeScript 會自動判斷傳進來的型別均為 never,這也是為什麼回傳 errorHandler 時不會發生錯誤,因為 errorHandler 為 never 型別。

src/main.ts
const errorHandler = (errorMessage: any) => {  throw new Error(errorMessage);};const numberOrString = (value: number | string): string => {  if (typeof value === "string") return "string";  if (typeof value === "number") return "number";  return errorHandler("Error");};

Assertions

如果使用 DOM 方法去取得元素控制權的話,TypeScript 會隨著取得的方式不同而給予不同的型別。

如果使用指定 ID 或 Class 的方式來取得元素,TypeScript 會不知道我們要取得的是哪種標籤元素,進而回傳 Element 型別。

Image

要是直接告訴 TypeScript,我們要取得的是 span 或 p,TypeScript 就會知道要回傳該元素的型別。

Image

所以要用 ID 、 Class 或其他選擇器取得元素的話,會用 as 關鍵字,也就是斷言(Assertion),直接定義型別,因為我們知道要取得的元素是什麼型別,但 TypeScript 不知道:

src/main.ts
const spanId = document.querySelector("#span") as HTMLSpanElement;const pId = document.querySelector("#p") as HTMLParagraphElement;

你可能也有看過這樣的寫法,這樣寫也是屬於斷言的一種:

src/main.ts
const spanId = <HTMLSpanElement>document.querySelector("#span");const pId = <HTMLParagraphElement>document.querySelector("#p");

Index Signatures & keyof Assertions

假設今天有一物件,名為 bill,而該物件對應的型別為 Menu

src/main.ts
interface Menu {  Pizza: number;  Burger: number;  Water: number;}const bill: Menu = {  Pizza: 200,  Burger: 120,  Water: 30,};console.log(bill.Pizza); //200

如果今天需要將 bill 物件內的值全部加起來,可能會這樣做:

src/main.ts
interface Menu {  Pizza: number;  Burger: number;  Water: number;}const bill: Menu = {  Pizza: 200,  Burger: 120,  Water: 30,};const todayBill = (): number => {  let total = 0;  for (const billKey in bill) {    // No index signature with a parameter of type 'string' was found on type 'Menu'.    total += bill[billKey];  }  return total;};

但 TypeScript 會跳出錯誤提示,說我們沒有添加 index signature,這通常會在動態載入的情況下發生。

要修正這個錯誤只要添加 index signature 即可:

src/main.ts
interface Menu {  [index: string]: number;}const bill: Menu = {  Pizza: 200,  Burger: 120,  Water: 30,};const todayBill = (): number => {  let total = 0;  for (const billKey in bill) {    total += bill[billKey];  }  return total;};console.log(todayBill());

這樣寫的意思是我們的物件內的 key 都是 string,而 key 對應的值都是 number。

如果不想提供 index signature 的話,則可以使用 keyof 取代之:

src/main.ts
interface Menu {  Pizza: number;  Burger: number;  Water: number;}const bill: Menu = {  Pizza: 200,  Burger: 120,  Water: 30,};const todayBill = (): number => {  let total = 0;  for (const billKey in bill) {    total += bill[billKey as keyof Menu];  }  return total;};console.log(todayBill());

或是搭配 typeof 直接從物件取得 type:

src/main.ts
interface Menu {  Pizza: number;  Burger: number;  Water: number;}const bill: Menu = {  Pizza: 200,  Burger: 120,  Water: 30,};const todayBill = (): number => {  let total = 0;  for (const billKey in bill) {    total += bill[billKey as keyof typeof bill];  }  return total;};console.log(todayBill());

Utility Types

Partial

如果只想提取物件內部分的值,不想全部提取的話,可以使用 Partial 關鍵字:

src/main.ts
interface Assignment {  studentId: string;  title: string;  grade: number;  verified?: boolean;}const updateAssignment = (  assign: Assignment,  propsToUpdate: Partial<Assignment>): Assignment => {  return { ...assign, ...propsToUpdate };};const assign1: Assignment = {  studentId: "1",  title: "Project",  grade: 0,};const assignGraded: Assignment = updateAssignment(assign1, { grade: 100 });

Required

如果在物件型別內有使用到 ? 可選關鍵字,但又想讓某個物件變數必須將所有型別都定義且附值,則可以使用 Required 關鍵字:

src/main.ts
interface Assignment {  studentId: string;  title: string;  grade: number;  verified?: boolean;}const assign1: Assignment = {  studentId: "1",  title: "Project",  grade: 100,};//因為有使用到 Required,所以 assign2 必須定義 verified。const assign2: Required<Assignment> = {  studentId: "1",  title: "Project",  grade: 100,  verified: true,};

Record

如果想限制變數內能使用的 key 和 value 的 type,則可以使用 Record 關鍵字:

src/main.ts
const hexColorMap: Record<string, string> = {  red: "FF0000",  green: "00FF00",  blue: "0000FF",};

同樣的也可以限制能輸入的值:

src/main.ts
type Students = "Wei" | "Yun";type Grades = "A" | "B" | "C" | "D";// key 只能是 Wei 或 Yun// value 只能是 A B C Dconst finalGrades: Record<Students, Grades> = {  Wei: "A",  Yun: "B",};

改為 interface:

src/main.ts
type Students = "Wei" | "Yun";interface Grades {  assign1: number;  assign2: number;}const finalGrades: Record<Students, Grades> = {  Wei: {    assign1: 100,    assign2: 90,  },  Yun: {    assign1: 95,    assign2: 90,  },};

Pick

如果想限制變數能使用的 key,則可以使用 Pick 關鍵字:

src/main.ts
interface Assignment {  studentId: string;  title: string;  grade: number;  verified?: boolean;}type AssignResult = Pick<Assignment, "studentId" | "grade">;// score 物件只能定義 studentId 和 gradeconst score: AssignResult = {  studentId: "1",  grade: 100,};

Omit

同上,如果想讓變數不能使用特定 key 的話,則可以使用 Omit 關鍵字:

src/main.ts
interface Assignment {  studentId: string;  title: string;  grade: number;  verified?: boolean;}type AssignResult = Omit<Assignment, "studentId" | "grade">;// score 物件不能定義 studentId 和 gradeconst score: AssignResult = {  title: "Project",};

Exclude

如果想把某個值排除在外的話,可以使用 Exclude 關鍵字:

src/main.ts
type Grades = "A" | "B" | "C" | "D";// type adjustedGrade = "A" | "B" | "C"type adjustedGrade = Exclude<Grades, "D">;

Extract

如果想把某個值提取出來的話,可以使用 Extract 關鍵字:

src/main.ts
type Grades = "A" | "B" | "C" | "D";// type highGrade = "A" | "B"type highGrade = Extract<Grades, "A" | "B">;

NonNullable

如果想排除 undefinednull 的話,則可以使用 NonNullable 關鍵字:

src/main.ts
type Grades = "A" | "B" | "C" | "D" | undefined | null;// type onlyValidValues = "A" | "B" | "C" | "D"type onlyValidValues = NonNullable<Grades>;

ReturnType

如果想要直接取得 Function Return 的型別又不想定義 Type 的話,可以使用 ReturnType 關鍵字:

src/main.ts
//不使用 ReturnTypetype newAssign = { title: string; points: number };const createNewAssign = (title: string, points: number): newAssign => {  return { title, points };};
src/main.ts
//使用 ReturnTypeconst createNewAssign = (title: string, points: number) => {  return { title, points };};// type NewAssign = {//     title: string;//     points: number;// }type NewAssign = ReturnType<typeof createNewAssign>;