# TypeScript ## TS 指定型別方法 1. Type Inference (型別推斷) 1. Type Annotation(型別註解) 1. Type Assertions(型別斷言) ### Type Inference (型別推斷) 如果沒有明確指定型別, TypeScript 會依照型別推斷(Type Inference)的規則推斷出一個型別。 ```javascript= //Type Inference (型別推斷) let x = 3; x = "hello";// error: Type 'number' is not assignable to type 'string'. ``` ### Type Annotation (型別註記) ```javascript! //Type Annotation (型別註記) let age: number = 18; // number variable let person: string = "iris"; // string variable let isUpdated: boolean = true; // boolean variable age = "32";//error: Type 'string' is not assignable to type 'number'. isUpdated = false; //ok ``` ### Type Assertions(型別斷言) ```javascript! interface Foo { age: number; name: string; } //語法1: 值 as 型別 const obj2 = {} as Foo; obj2.age = 18; obj2.name = "iris"; const obj3 = { age: 18, name: "iris" } as Foo; //語法2: <型別>值 const obj4 = <Foo>{ age: 18, name: "iris" }; ``` ## 型別有哪些? * **原始型別 Primitive Types**:包含 number, string, boolean, undefined, null, ES6 介紹的 symbol 與時常會在函式型別裡看到的 void * **物件型別 Object Types**,但我個人還會再細分成小類別,但這些型別的共同特徵是 —— 從原始型別或物件型別組合出來的複合型態(比如物件裡面的 Key-Value 個別是 string 和 number 型別組合成的): * **基礎物件型別**:包含 JSON 物件,陣列`(Array<T>或T[])`,類別以及類別產出的物件(也就是 Class 以及藉由 Class new 出來的 Instance) * **TypeScript 擴充型別**:即 Enum 與 Tuple,內建在 TypeScript * **函式型別 Function Types**:類似於 (input) => (Ouput) 這種格式的型別,後面會再多做說明 * **明文型別 Literal Type**:一個值本身也可以成為一個型別,比如字串 "Hello world" —— 若成為某變數的型別的話,它只能存剛好等於"Hello world" 字串值;但通常會看到的是 Object Literal Type,後面也會再多做說明 * **特殊型別**:筆者自行分類的型別,即 any、never(TS 2.0釋出)以及最新的 unknown 型別(TS 3.0釋出),讀者可能覺得莫名其妙,不過這些型別的存在仍然有它的意義,而且很重要,陷阱總是出現在不理解這些特殊型別的特性 * **複合型別**:筆者自行分類的型別,即 union 與 intersection 的型別組合,但是跟其他的型別的差別在於:這類型的型別都是由邏輯運算子組成,分別為 | 與 & * **通用型別 Generic Types**:留待進階的 TypeScript 文章介紹,一種讓程式碼可以變得更加通用的絕招 ## Nullable Types (null 跟 undefined) Nullable Types 或者被視為 any 型別的變數可以隨隨便便地使用。 ![image alt](https://ithelp.ithome.com.tw/upload/images/20190911/201206144EiMQkd2ie.png) ## 物件型別 Object Types : object ```javascript! // object 型別註記 let person2: object = { name: "iris", age: 18 }; ``` ❌ 無法單獨對該物件屬性做覆寫,即使相同型別的值也無法 ❌ 無法單獨新增屬性 ❌ 無法刪除屬性 ✅ 可以完全覆寫整個物件(新增/減少屬性,即使型別完全不同都可以) ```javascript! // 一般 JSON 物件格式 let person = { name: "iris", age: 18 }; ``` ✅ 覆寫的值需與屬性對應的型別相同 ✅ 對物件整體覆寫,其覆寫的物件格式必須完全相同 ❌ 不能隨意新增原先不存在該物件的屬性 ❌ 不能覆寫整個物件時的格式錯誤(少一個 key / 新增 key / key所對應的值型別錯誤) ❓ 但如果 delete 屬性 TS 不會警告 ,還能夠進行刪除,不小心刪掉就GG (沒開嚴謹模式的時候) ❌ 補充:tsconfig 開啟嚴謹模式時,刪除屬性時 compiler 會報錯提醒。 ```javascript! // 一般 JSON 物件格式 let person = { name: "iris", age: 18 }; person.name = "aka 廢廢前端" //ok 型別相同 person = { name: "aa", //ok 覆寫的物件格式必須完全相同 age: 20 } person.name = false; // error Type 'boolean' is not assignable to type 'string'. person.job = "在家躺" //error Property 'job' does not exist on type person = { name: "bb" //error Property 'age' is missing in type } person = { gender : "male", //error Type '{ gender: string; job: string; }' is not assignable to type '{ name: string; age: number; }' job : "在家躺" } //沒開嚴謹模式的時候 delete person.name; //可執行刪除屬性 //tsconfig 開啟嚴謹模式 會報錯提醒 delete person.name;//error: The operand of a 'delete' operator must be optional. // object 型別註記 let person2: object = { name: "iris", age: 18 }; person2.name = "aka 廢廢前端" //error Property 'name' does not exist on type 'object'. person2 = { name: "aa", //ok age: 20 } person2.name = false; // error Property 'name' does not exist on type 'object'. person2.job = "在家躺" //error Property 'job' does not exist on type 'object'. person2 = { name: "bb" //ok } person2 = { gender : "male", //ok job : "在家躺", } delete person2.name; //error Property 'name' does not exist on type 'object'. ``` ## 可選屬性 (Optional Properties) 我們可以去指定部分或全部屬性為可選屬性,方法是在屬性名稱後面加上一個?。 ```javascript! //可選屬性 (Optional Properties) const getUserInfo2 = (person5: { name: string, age?: number }) =>{ if(person5.age !== undefined){ console.log(`Hello, my name is ${person5.name}. I'm ${person5.age} years old.`); }else{ console.log(`Hello, my name is ${person5.name}.`); } } getUserInfo2({ name: "iris"}); //Hello, my name is iris. getUserInfo2({ name: "iris", age: 18 }); //Hello, my name is iris. I'm 18 years old. ``` ## Arrays 定義陣列型別 ```javascript! //arrays //1.一般方括號寫法 const list1 = [1, 2, 3]; //2.「型別 + 方括號」表示法 const list2: number[] = [1, 2, 3]; //3.陣列泛型 const list3: Array<number> = [1, 2, 3]; //4. 用 interface (介面)表示陣列 interface NumberArray { [index: number]: number; } const list4: NumberArray = [1, 1, 2, 3, 5]; ``` ## Function 定義陣列型別 ```javascript! //Parameter Type Annotations 參數型別註釋 function greet(name: string) { console.log("Hello, " + name.toUpperCase() + "!!"); } //Return Type Annotations 返回值型別註釋 function getFavoriteNumber(): number { return 26; } // -------------------------------------------- //Function Declaration 函式宣告 function sum(x: number, y: number): number { return x + y; } console.log(sum(1,2)); //3 //Function Expression 函式表示式 let sum2 = function (x: number, y: number): number { return x + y; }; console.log(sum2(1,2)); //3 // Arrow Function 箭頭函式 let sum3 = (x: number, y: number): number => x + y; console.log(sum3(1,2)); //3 //Anonymous Functions 匿名函式 // 在 TypeScript 中使用匿名函數時, 他會自動去推斷參數型別 const names = ["Alice", "Bob", "Eve"]; names.forEach( (s) => { console.log(s.toUpperCase()); }); ``` ## any 1. 在 any型別下,可以賦值給任何型別,使用任何屬性和方法,都是被允許的。 1. 變數如果在宣告的時候,未指定其型別,那麼它會被識別為any型別 ```javascript! //** any **/ let myFavoriteNumber: any = 'seven'; myFavoriteNumber = 7; let obj: any = { x: 0 }; obj.bar = 100; obj = "hello"; const n: number = obj; const s: string = obj; const b: boolean = obj; console.log(n); //hello //未宣告型別的變數及參數都為any //例子1: let something; something = 7; something = "seven"; //例子2: function fn(s) { console.log(s.subtr(3)); } fn(42); ``` ## unknown unknown 只能賦值給 any 和自己。 ```javascript! //** unknown **/ function f1(a: any) { a.b(); // OK } function f2(a: unknown) { a.b(); //error: Property 'b' does not exist on type 'unknown'. } let value: unknown; let value1: unknown = value; // ok let value2: any = value; // ok let value3: boolean = value; // error let value4: number = value; // error let value5: string = value; // error let value6: object = value; // error let value7: any[] = value; // error let value8: Function = value; // error ``` ## void void 表示沒有任何返回值的函式 ```javascript! //** void **/ function alertName(): void { alert('My name is iris'); } ``` ## never 來表示不應該存在的狀態的型別,一般用於錯誤處理函式。 ```javascript= //** never **/ function error(message: string): never { throw new Error(message); } function fn2(x: string | number) { if (typeof x === "string") { // do something } else if (typeof x === "number") { // do something else } else { x; // has type 'never'! } } ``` ## Union Types(聯合型別) 表示取值可以為多種型別中的其中一種 ```javascript= //** union types(聯合型別) **/ function printId(id: number | string) { console.log("Your ID is: " + id); } printId(101); //ok printId("202"); //ok //typeof function printId2(id: number | string) { if (typeof id === "string") { //型別為字串才 toUpperCase console.log(id.toUpperCase()); } else { //其他自動判定為 number 型別 console.log(id); } } printId2("ABC"); //Array.isArray() function welcomePeople(x: string[] | string) { if (Array.isArray(x)) { console.log("Hello, " + x.join(" and ")); } else { console.log("Welcome lone traveler " + x); } } welcomePeople(["a","b","c"]); ``` ## Intersection Types (交集型別) 表示其定義的值都必須同時符合兩種型別。 ```javascript= //** Intersection types (交集型別) **/ //Intersection 在 primitive type 中使用,是無法同時滿足兩種型別的,會被認定為 never 型別。 function printId3(id: number & string) { console.log("Your ID is: " + id); } printId3(101); //error printId3("202"); //error //主要用來組合現有的型別,若都沒符合兩種型別,則會報錯提醒。 interface Colorful { color: string; } interface Circle { radius: number; } type ColorfulCircle = Colorful & Circle; //帶入的參數需滿足這兩個型別 function draw(circle: ColorfulCircle) { console.log(`Color was ${circle.color}`); console.log(`Radius was ${circle.radius}`); } draw({ color: "blue", radius: 42 });// ok draw({ color: "red", raidus: 42 }); //error ``` ## Literal Types 字面值型別 ```javascript= //** Literal Types 字面值型別 **/ //string literal types //example1 let x: "hello" = "hello"; x = "hello"; //ok x = "howdy"; //Type '"howdy"' is not assignable to type '"hello"' //example2 function printText(s: string, alignment: "left" | "right" | "center") { console.log(`${s} placed at the ${alignment}`) } printText("Hello, world", "left"); printText("G'day, mate", "centre"); //error: Argument of type '"centre"' is not assignable to parameter of type '"left" | "right" | "center"'. //numeric literal types function compare(a: string, b: string): -1 | 0 | 1 { return a === b ? 0 : a > b ? 1 : -1; } //non-literal types interface Options { width: number; } function configure(x: Options | "auto") { console.log(x); } configure({ width: 100 }); configure("auto"); configure("automatic"); // 不符Options 及 “auto” //error: Argument of type '"automatic"' is not assignable to parameter of type 'Options | "auto"'. ``` ## Tuple 元組 就是合併了不同型別的物件 ```javascript= //** Tuple 元組 **/ let tom: [string, number]; tom = ['tom', 18]; //如果只有宣告tom沒賦值,會是undefined,tsconfig strictNullChecks 打開的話會報錯提醒 tom[0] = 'Tom'; //ok tom[1] = 25; //ok tom[0].slice(1); //ok tom[1].toFixed(2); //ok tom.push('male'); //ok tom = ['tom chen']; //error:Property '1' is missing in type '[string]' but required in type '[string, number]'. tom.push(true); //error: Argument of type 'true' is not assignable to parameter of type 'string | number'. ``` ## Enums 列舉 列舉(Enum)型別用於取值被限定在一定範圍內的場景,比如一週只能有七天,顏色限定為紅綠藍等。 ### 簡單的例子 ```javascript= enum Days {Sun, Mon, Tue, Wed, Thu, Fri, Sat}; console.log(Days["Sun"] === 0); // true console.log(Days["Mon"] === 1); // true console.log(Days["Tue"] === 2); // true console.log(Days["Sat"] === 6); // true console.log(Days[0] === "Sun"); // true console.log(Days[1] === "Mon"); // true console.log(Days[2] === "Tue"); // true console.log(Days[6] === "Sat"); // true ``` ### 手動賦值 ```javascript enum Days {Sun = 7, Mon = 1, Tue, Wed, Thu, Fri, Sat}; console.log(Days["Sun"] === 7); // true console.log(Days["Mon"] === 1); // true console.log(Days["Tue"] === 2); // true console.log(Days["Sat"] === 6); // true // 要小心覆蓋的情況 enum Days {Sun = 3, Mon = 1, Tue, Wed, Thu, Fri, Sat}; console.log(Days["Sun"] === 3); // true console.log(Days["Wed"] === 3); // true console.log(Days[3] === "Sun"); // false console.log(Days[3] === "Wed"); // true // 手動賦值的列舉項也可以為小數或負數 enum Days {Sun = 7, Mon = 1.5, Tue, Wed, Thu, Fri, Sat}; console.log(Days["Sun"] === 7); // true console.log(Days["Mon"] === 1.5); // true console.log(Days["Tue"] === 2.5); // true console.log(Days["Sat"] === 6.5); // true ``` ## 常數項和計算所得項 列舉項有兩種型別:**常數項**(constant member)和**計算所得項**(computed member)。 ### 常數列舉 常數列舉是使用 const enum 定義的列舉型別。 加了 const 關鍵字後,列舉在編譯時不會產生查找物件,在常數列舉中無法使用計算值,且編譯時會將列舉的元素引用替換成其值。 常數列舉和上面的普通列舉最大的差異就是 1. 常數列舉不會產生查找物件,可以提升效能 1. 常數列舉的元素只能是常數,不能是運算值 ```javascript= const enum Directions { Up, Down, Left, Right } let directions = [Directions.Up, Directions.Down, Directions.Left, Directions.Right]; ``` ## 計算所得項 ```javascript= enum Color {Red, Green, Blue = "blue".length}; console.log(Color.Blue); //4 // 像是二元運算子 << 、 |、& 等都歸為常數項: enum FileAccess { // constant members None, Read = 1 << 1, Write = 1 << 2, ReadWrite = Read | Write, // computed member G = "123".length, } ``` ## Ambient Enums 外部列舉 外部列舉(Ambient Enums)是使用 declare enum 定義的列舉型別: ```javascript= declare enum Directions { Up, Down, Left, Right } let directions = [Directions.Up, Directions.Down, Directions.Left, Directions.Right]; ``` ## Interfaces TS 的核心原則就是型別檢查,而介面(Interface)正是在 TS 中用來定義抽象物件的資料型別。介面的概念可以想像成是訂定契約,而使用此契約的物件或類別就一定會符合契約中所規定的規範,如果不符合,TS 就會報錯,但是介面只會定義描述有哪些方法(Method)和屬性(Property),無法實作。 - 變數比介面多或少一些屬性是不允許的。 ```javascript= interface IPerson { name: string; age: number; gender?: string; } const iris: IPerson = { name: 'Iris', age: 18 }; ``` ### 可選屬性(Optional Properties) - 有些屬性不一定必須,可能是某些條件下存在,這時候就可以使用之前在做型別註記的時候有使用到的可選屬性,在屬性名稱後方加上?即可 ```javascript= interface Phone { model: string, price?: number } let myPhone: Phone = { model: 'iPhone', } ``` ### Index Signatures Index Signatures 就是當我們有時候事先並不知道屬性名稱,或是未來會新增屬性,但知道值的形狀。我們就可以使用 Index Signatures 來描述值可能的型別。 ```javascript= interface IPerson { name: string; age?: number; // 使用[propName: string]定義了key, 屬性名稱為字串,賦值時可自行命名。對應的值 value 的型別為 string 或者 number 或是 undefined 。 [propName: string]: string | number | undefined; } const iris5: IPerson = { name: 'Iris', gender: 'female' }; ``` ### 唯讀屬性 readonly 可以用 readonly 定義唯讀屬性,唯讀的約束存在於第一次給「物件」賦值的時候,而不是第一次給「唯讀屬性」賦值的時候。 ```javascript= interface IPerson { readonly id: number; //唯讀約束 name: string; age?: number; [propName: string]: any; } const iris: IPerson = { id: 89757, //如果沒給則報錯 Property 'id' is missing in type name: 'Iris', gender: 'female' }; ``` ### Extending Types 擴展 - 使用 extends 來多增加 unit 的欄位 - interface 也可以擴展多個 interface ```javascript= interface BasicAddress { name?: string; postalCode: string; } interface AddressWithUnit extends BasicAddress { unit: string; } // ------------- 擴展多個 -------------------- interface Colorful { color: string; } interface Circle { radius: number; } interface ColorfulCircle extends Colorful, Circle { background: string; } const cc: ColorfulCircle = { color: "red", radius: 42, background: "white" }; ``` ### interface 可以重複定義 - 重複相同的interface 名稱, TS會進行合併 ```javascript= interface IPerson { name: string; } interface IPerson { age?: number; } interface IPerson { gender: string; } const iris:IPerson = { name: 'Iris', age: 18 } //error:Property 'gender' is missing in type '{ name: string; age: number; }' but required in type 'IPerson6' ``` ### interface 還可以定義陣列及函式 ```javascript= interface INumberArray { [index: number]: number; } const list: INumberArray = [1, 1, 2, 3, 5]; ``` ### interface 可以 function overload - function overload 是指擴充一個函式可以被執行的形式。 ```javascript= interface IPoint { getDist(): number; getDist(x?: number): number; } const point:IPoint = { getDist(x?: number) { if (x && typeof x == "number") { return x; } else { return 0; } } } console.log(point.getDist(20)); //20 console.log(point.getDist()); //0 ``` ## Generics Types 泛型 指在定義宣告時不預先指定具體的型別,而是執行的時候才確認型別的特殊方式,經常用在函式、介面或類別等型別。 ```javascript= // 在函式後面加上<T>,其中 T 為Type variables(型別變數)用來指代任意輸入的型別,之後就可以使用 T 作為回傳值同型別的指代。 function foo<T>(arg: T): T{ return arg; // 傳遞型別參數 foo<number> // 傳遞函式參數(自動判斷型別參數) foo(1); // 傳遞函式參數及型別參數 foo<number>(1); ``` ### Generic Constraints 泛型限制 這裡使用 extends 關鍵字讓 T 為 foodie 的擴展,限縮了 T 的型別,讓 T 不再適用於任何型別,如此,TS 在編譯時就不會報錯了。 現在這個通用函式被限縮了型別,傳入的參數就必須包含介面中設定的屬性。 ```javascript! interface foodie{ length: number } function foo<T extends foodie>(arg: T):T{ console.log(arg.length) return arg } foo(3) // Error: Argument of type '3' is not assignable to parameter of type 'foodie' foo({length:10}) // 10 // 若傳入多的參數則沒關係 foo({length:10, name:'Kira'}) ``` ### Type Alias 也能使用泛型 ```javascript= // 範例1 type Person<T> = { age: T; }; const john1: Person<number> = { age: 30, }; const john2: Person<string> = { age: '二十', }; // 範例2------------------------- type PersonNameType { firstName: string; lastName: string } type Person<T extends PersonNameType> = T; const pjchender: Person<{ firstName: string; lastName: string; occupation: string; }> = { firstName: 'PJ', lastName: 'Chen', occupation: 'developer', }; ``` ## Type Aliases(型別別名) vs Interfaces(介面)差異 ### 相同處 * 都可使用 readonly / 可選屬性 / 新增任意屬性 / function overload * 都可以 extend , 只是用法不同 * 都可以被 class implement(但要特別注意, 如果是 union type 是無法被 implements 的。) #### 繼承範例 ```javascript= // Extending an interface: interface Animal { name: string } interface Bear extends Animal { honey: boolean } ``` ```javascript= // Extending a type via intersections: type Animal = { name: string } type Bear = Animal & { honey: boolean } ``` ```javascript= // interface extends type: type Name = { name: string; } interface User extends Name { age: number; } ``` ```javascript= // Extending interface & type via intersections: interface Name { name: string; } type User = Name & { age: number; } ``` #### 實例化範例 interface: ```javascript= interface Point { x: number; y: number; } class SomePoint implements Point { x = 1; y = 2; } ``` type: ```javascript= type Point2 = { x: number; y: number; }; class SomePoint2 implements Point2 { x = 1; y = 2; } ``` ### 不同處 interface 名稱可以重複定義並合併 , type 不行 ### 使用情境的比較 * 單純想表示靜態格式資料概念時使用type,希望資料被重複多方利用時使用 interface * 若是原始資料型別、列舉(Enum)和元組(Tuple)型別和複合型別,通常只能使用 type 進行宣告 * 若是物件格式 Interface 和 Type 都可以進行宣告,但建議使用 Interface 比較彈性 * Interface 和 Type 可以混用擴展,但使用 extends 和 union 或 intersection 擴展代表的含義不同: * 不希望再被擴充或靜態的型別格式就應該用 type 宣告 type,藉由 union 或 intersection 達成擴展 * 希望之後被擴充或多方利用,則應該宣告成 interface,藉由 extends 去達成擴展 # class 使用 ## class是什麼? 簡單來說就是物件的模板,定義了一件事物的抽象特點,包含它的屬性和方法,提供一個更簡潔的語法來建立物件和處理繼承。 類別由三個元素組成:建構函式、屬性和方法。 另外,建構函式不一定需要,若有建構函示則會只有一個,預設會是空函式(即 function() {}),通常只有需要進行初始化成員變數才需要建構函式。 ## class基本使用與概念 創建該物件應有的屬性與方法 ```javascript= class Car { constructor(color) { this.color = color; //公開屬性 } getDescription() { //公開方法 return `我是車子 -${this.color}` } } const blackCar = new Car("黑色"); const redCar = new Car("紅色"); console.log(blackCar.getDescription()); // 我是車子 - 黑色 console.log(blackCar.color); // 黑色 ``` ## getters & setter getter method 是在 class 中沒有帶參數就可以回傳值的方法,可以透過 get 設定;setter method 則是可以透過 = 來對物件賦值,可以使用 set 來設定: * 存取器主要用在對 private 私有屬性進行間接地操作,如此,可以提高屬性的安全性,同時又保證屬性的封閉性。 * getter 主要模擬呼叫物件的屬性時的行為,setter模擬指賦值到物件屬性的行為,兩者皆是使用函式做到屬性的讀取 * 若僅有實踐某物件屬性的 getter,沒有setter,則該物件屬性自動推斷為唯獨狀態(readonly) * getter 不能有參數,且一定要有回傳值; setter 只能有一個參數 ```javascript= class Person { constructor({ firstName, lastName, country = 'Taiwan' } = {}) { this.firstName = firstName; this.lastName = lastName; this.country = country; } // getter method get name() { return this.firstName + ' ' + this.lastName; } // setter method set name(input) { [this.firstName, this.lastName] = input.split(' '); } } let aaron = new Person({ firstName: 'Aaron', lastName: 'Chen' }); console.log(aaron.name); // 使用 getter method,Aaron Chen aaron.name = 'Peter Chen'; // 使用 setter method console.log(aaron.name); // Peter Chen ``` ## Private class fields 為了解決最初希望有 private variable 的情況,多了一種 private class fields 的語法,你只需要在變數名稱的最前方加上 # 即可,而後外部無法讀取該屬性,且無法繼承。 ```javascript= class IncreasingCounter { #count = 0; get value() { console.log('Getting the current value!'); return this.#count; } increment() { this.#count++; } } // 無法繼承 若使用則會爆錯 // Private field '#count' must be declared in an enclosing class // // class childCounter extends IncreasingCounter{ // getCount() { // return this.#count // } // } const counter = new IncreasingCounter(); counter.increment(); counter.value //1 counter.#count; // → SyntaxError counter.#count = 42; // → SyntaxError ``` ## Static method(靜態方法) static method 只存在 class 中,不能被 instance 所提取,只能透過指稱到該 class 才能使用該方法,可以透過 static 這個關鍵字來建立 static method ```javascript= class Point { //建構初始物件 constructor(x = 0, y = 0) { //預設給0 this.x = x; this.y = y; } //static method without this static distance(a, b) { const dx = a.x - b.x; const dy = a.y - b.y; return Math.sqrt(dx * dx + dy * dy); } } //static let p1 = new Point(10, 12); let p2 = new Point(16, 18); console.log(Point.distance(p1, p2)); //8.48528137423857 ``` ## Static and public/private properties 透過 class field syntax 的寫法,一樣可以用來建立公開(public)或私有(private)的靜態屬性(static property)或靜態方法(static method) ```javascript= class FakeMath { // `PI` 是一個靜態的公開屬性 // `PI` is a static public property. static PI = 22 / 7; // Close enough. // `#totallyRandomNumber` 是一個靜態的私有屬性 static #totallyRandomNumber = 4; // `#computeRandomNumber` 是一個靜態的私有方法 static #computeRandomNumber() { return FakeMath.#totallyRandomNumber; } // `random` 是一個靜態的公開方法 (ES2015 syntax) // that consumes `#computeRandomNumber`. static random() { console.log('I heard you like random numbers…'); return FakeMath.#computeRandomNumber(); } } FakeMath.PI; // → 3.142857142857143 FakeMath.random(); // logs 'I heard you like random numbers…' // → 4 FakeMath.#totallyRandomNumber; // → SyntaxError FakeMath.#computeRandomNumber(); // → SyntaxError ``` ## Class fields syntax: public/private class fields Public, Private, Static 的差別: * Public variable:class 內的變數可以被 instance 所存取和修改 * Private variable:class 內的變數無法被**外部**讀取與繼承該屬性 * Static variable / static method:靜態表示該方法或變數,需要直接使用該類(而非實例化過的物件),才能取得或使用。 ## Class 擴展/繼承 -super、 extends 關鍵字 super: super為關鍵字,有兩種用法,其一為傳遞變數給父類別,其二,呼叫父類別方法。必須出現在this 關鍵字之前使用。 ```javascript= class Car { constructor(color) { this.color = color; } getDescription() { //公開方法 return `我是車子 -${this.color}` } } class CarV2 extends Car{ constructor(color, version) { // 在這裡執行的 super 等同於父類別的 constructor super(color); this.version = version; } getDescription2() { //使用super呼叫父類別方法 return `${super.getDescription()} ${this.version} 第二代強化版`; } } const greenCar = new CarV2('綠色', 23); console.log(greenCar.getDescription2()); ``` # typescript + class 使用 ## 存取修飾符(Access Modifiers) 存取修飾符用來限制類別中的屬性或方法在類別內部或外部被呼叫的權限。 * public - 屬性或方法是公開的,可以任意任何地方使用(預設值) * private - 屬性或方法是私有的,只能在本身類別內部使用 * protected - 屬性或方法受到保護,只能在本身類別內以及繼承的子類別中使用 | 是否可存取 | public | private| protected | | -------- | -------- | -------- |-------| | 類別本身 | 可以 | 可以 | 可以 | 繼承的子類別 | 可以 | 不行 | 可以 | 類別的物件實例 | 可以 | 不行 | 不行 ```javascript= class Foo { constructor(theX: number, theY:number, theZ: number) { this.x = theX; this.y = theY; this.z = theZ } public x: number; private y: number; protected z: number; } // 將類別實例化,創造新物件 foo class Foo { constructor(theX: number, theY:number, theZ: number) { this.x = theX; this.y = theY; this.z = theZ } public x: number; private y: number; protected z: number; } // 繼承的子類別 FooChild class FooChild extends Foo { constructor(x:number, y:number, z: number) { super(x,y,z); } handleChange(n:number){ this.x = n; this.y = n; //Property 'y' is private and only accessible within class 'Foo'. this.z = n; } } let foo = new FooChild(2,3,4); console.log(foo.x) //2 console.log(foo.y) // Property 'y' is private and only accessible within class 'Foo'. console.log(foo.z) //Property 'z' is protected and only accessible within class 'Foo' and its subclasses. foo.handleChange(6) ``` ## Abstract 抽象 abstract是 TS 1.6版本加入的新關鍵字,可以加在類別名稱或類別方法的前面,用來限制類別的實例化,特性摘要如下: 1. abstract 加在類別名稱前,表示類別僅供其他類別繼承/擴展,但不允許使用 new 關鍵字進行實例化 1. abstract 加在類別方法前,表示此方法不允許實例化 1. 倘若類別中有抽象方法,則該類別一定要註記為抽象類別 1. 抽象類別方法若要加上存取修飾符,則存取修飾符必須在 abstract 關鍵字前面 1. 抽象類別可以繼承/擴展,但繼承的子類別必須實踐抽象方法 ```typescript= class A { // ... } abstract class B { foo(): number { return bar(); } abstract bar() : number; } new B; // Error : B為抽象類別,無法創建實例 class C extends B { } // Error : 非抽象類別C繼承抽象類別B,必須實作抽象方法bar() abstract class D extends B { } // OK class E extends B { // OK:有實作抽象方法 bar() { return 1; } ``` 搭配 public 、 protected 和 private 一起使用 ```typescript= class MyClass { private static x = 0; } console.log(MyClass.x); //error:Property 'x' is private and only accessible within class 'MyClass'. ``` ## 靜態屬性與方法(Static Properties an methods) 在 ES6 的 Class 中只有靜態屬性,沒有靜態方法,而在 TS 中,靜態屬性和方法都有。 預設狀況下,所有在類別中定義的方法和屬性都會被實例繼承,但如果加上 static 關鍵字,轉換成靜態屬性和方法後,**則表示該方法或屬性不會被實例繼承,僅存在類別內部**,倘若要使用會直接透過類別來調用。 ```typescript= class Something { static instances = 0; static foo():number { return 42; } } //繼承的子類別SomethingMore class SomethingMore extends Something { } console.log(SomethingMore.foo()); // 42 可繼承父類別的靜態方法 //實例化物件s1 const s1 = new Something(); console.log(s1.instances) //Error: Property 'instances' is a static member of type 'Something' console.log(SomethingMore.instances); // 0 可繼承父類別的靜態屬性 ``` ## 實踐(Implements) 介面(Interface)主要表示抽象的行為,不能初始化屬性和方法,當類別實踐(implement)介面時就可以具體化行為了。 ```typescript= interface Alarm { alert():void; } class Door { } class SecurityDoor extends Door implements Alarm { alert() { console.log('SecurityDoor alert'); } } class Car implements Alarm { alert() { console.log('Car alert'); } } // 創建利用Car類別作為原型的新物件 const car: Car = new Car(); car.alert(); //我們定義了一個警報器的功能,但是具體的警報行為則是在車子類別和防盜門類別實踐(implement)的過程中才定義。 ``` 一個類別可以實踐多個介面 ```typescript= interface Alarm { alert():void; } interface Light { lightOn():void; lightOff():void; } class Car implements Alarm, Light { alert() { console.log('Car alert'); } lightOn() { console.log('Car light on'); } lightOff() { console.log('Car light off'); } } ``` # 額外閱讀 - [What are the differences between the private keyword and private fields in TypeScript?](https://stackoverflow.com/questions/59641564/what-are-the-differences-between-the-private-keyword-and-private-fields-in-types) - [JavaScript 私有类字段和 TypeScript 私有修饰符](https://juejin.cn/post/6844904052174635015) - 結論: typescript 的 private 修飾符 與 js 的 private (#) 的差異在於,typescript 的 private 在編譯完後並非為真正的私有屬性。而若在 typescript 使用 #,編譯版本最低必须是 ECMAScript 2015