[typescript] TypeScript 基礎 (使用 Vite 建立環境)
說明
要快速建立 TypeScript 的專案,可以使用 Vite,在 Terminal 輸入以下的指令即可:
npm create vite@latest 你的專案名稱 -- --template vanilla-ts
接著切換到你的專案目錄,執行以下指令,就把基本的環境建置好了:
npm install
將環境 Run 起來:
npm run dev
Union
要讓一個變數擁有多種的型別,可以使用 Union
:
let userId: string | number = ""; // userId 只能是 string 或 number 型別userId = true; //Type 'boolean' is not assignable to type 'string | number'.
Tuple
如果要嚴謹的定義陣列內的變數型別,則可以使用 Tuple
:
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:
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 有的屬性定義出來,不然同樣會發生錯誤:
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
這個屬性:
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
內的屬性變為可選的,只要在屬性名稱後加上 ?
即可:
type Person = { name: string; age: number; friends: (string | number)[]; email?: string;};let Wei: Person = { name: "Wei", age: 25, friends: [1, 2, 3],};
Interface & Extends
假設我們有兩種型別需要定義,Person
和 Employee
,對某些物件來說可能只會用到 Person 內定義的屬性,並不會使用到 Employee 內定義的屬性:
type Person = { personName: string; age: number; friends: (string | number)[]; email?: string;};type Employee = { employeeName: string; salary: number;};
這時候如果用 type
來實作的話,可以寫成這樣:
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
延伸其他先前定義的屬性:
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
將型別儲存成一個樣板:
type stringOrNumber = string | number;type stringOrNumberArray = (string | number)[];type Person = { id: stringOrNumber; friends: stringOrNumberArray;};
Literal Types
我們也可以指定變數能填寫的值:
let username: "Wei" | "Yun" | "John";//Type '"Alex"' is not assignable to type '"Wei" | "Yun" | "John"'.username = "Alex"; //
Function
如果 Function 需要接收外部參數,則需要定義參數的型別:
//Parameter 'a' implicitly has an 'any' type.//Parameter 'b' implicitly has an 'any' type.const add = (a, b) => { return a + b;};
需更改成這樣:
const add = (a: number, b: number) => { return a + b;};
也可以指定 Function 的回傳值型別:
const add = (a: number, b: number): number => { return a + b;};
如果沒有任何回傳值的話,可以寫成 void
:
const log = (message: any): void => { console.log(message);};
也可以更改成 Type Aliases
的方式:
type addFunction = (a: number, b: number) => number;const add: addFunction = (a, b) => { return a + b;};
用 interface
來寫的話,則寫成以下:
interface addFunction { (a: number, b: number): number;}const add: addFunction = (a, b) => { return a + b;};
Function 接收的外部參數,可以給予預設值:
const add = (a: number = 10, b: number) => { return a + b;};add(undefined, 10); // 20add(15, 10); // 25
我們可以使用 Rest Parameters
來將傳遞進 Function 的參數一併處理:
const sum = (...nums: number[]) => { return nums.reduce((acc, cur) => acc + cur, 0);};sum(1, 2, 3, 4); // 10
如果有一個不會有回傳值的 Function,則 TypeScript 會自動將它定義為 never
,像是 無限迴圈
和 錯誤處理
。
另外要特別提一點,never
是所有型別的子型別(Bottom Type)。
const infiniteLoop = () => { let i: number = 0; while (true) { i++; }};const errorHandler = (errorMessage: any) => { throw new Error(errorMessage);};
never
型別較常用在錯誤處理的方面,像是 Type Guard
,如果傳入 string 或 number 以外的型別,TypeScript 會自動判斷傳進來的型別均為 never,這也是為什麼回傳 errorHandler
時不會發生錯誤,因為 errorHandler
為 never 型別。
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 型別。
要是直接告訴 TypeScript,我們要取得的是 span 或 p,TypeScript 就會知道要回傳該元素的型別。
所以要用 ID 、 Class 或其他選擇器取得元素的話,會用 as
關鍵字,也就是斷言(Assertion),直接定義型別,因為我們知道要取得的元素是什麼型別,但 TypeScript 不知道:
const spanId = document.querySelector("#span") as HTMLSpanElement;const pId = document.querySelector("#p") as HTMLParagraphElement;
你可能也有看過這樣的寫法,這樣寫也是屬於斷言的一種:
const spanId = <HTMLSpanElement>document.querySelector("#span");const pId = <HTMLParagraphElement>document.querySelector("#p");
Index Signatures & keyof Assertions
假設今天有一物件,名為 bill
,而該物件對應的型別為 Menu
。
interface Menu { Pizza: number; Burger: number; Water: number;}const bill: Menu = { Pizza: 200, Burger: 120, Water: 30,};console.log(bill.Pizza); //200
如果今天需要將 bill 物件內的值全部加起來,可能會這樣做:
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
即可:
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
取代之:
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:
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
關鍵字:
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
關鍵字:
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
關鍵字:
const hexColorMap: Record<string, string> = { red: "FF0000", green: "00FF00", blue: "0000FF",};
同樣的也可以限制能輸入的值:
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:
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
關鍵字:
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
關鍵字:
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
關鍵字:
type Grades = "A" | "B" | "C" | "D";// type adjustedGrade = "A" | "B" | "C"type adjustedGrade = Exclude<Grades, "D">;
Extract
如果想把某個值提取出來的話,可以使用 Extract
關鍵字:
type Grades = "A" | "B" | "C" | "D";// type highGrade = "A" | "B"type highGrade = Extract<Grades, "A" | "B">;
NonNullable
如果想排除 undefined
和 null
的話,則可以使用 NonNullable
關鍵字:
type Grades = "A" | "B" | "C" | "D" | undefined | null;// type onlyValidValues = "A" | "B" | "C" | "D"type onlyValidValues = NonNullable<Grades>;
ReturnType
如果想要直接取得 Function Return 的型別又不想定義 Type 的話,可以使用 ReturnType
關鍵字:
//不使用 ReturnTypetype newAssign = { title: string; points: number };const createNewAssign = (title: string, points: number): newAssign => { return { title, points };};
//使用 ReturnTypeconst createNewAssign = (title: string, points: number) => { return { title, points };};// type NewAssign = {// title: string;// points: number;// }type NewAssign = ReturnType<typeof createNewAssign>;