# TYPESCRIPT 此筆記是根據這門課程所製作而成的 https://www.udemy.com/course/understanding-typescript/ > [name=Layors] from 2022/10/11 Typescript是JavaScript的超集 TypeScript並不能像JavaScript一樣被瀏覽器所執行,也無法被node.js執行 ->TypeScript會把自己編譯回JavaScript檔 TypeScript追加了JavaScript沒有的型別 如果型別不對會報錯,但一樣會把js檔編譯出來 typeof可以用來確認某變數的型別,而這是js的東西 而且他在執行時才會作用,而非編譯時 ## Basic & type TS給予型別的方式:在變數後面加冒號再加型別 ```typescript= function add(n1: number, n2: number) { return n1 + n2; } ``` ### core type 通通都寫小寫 在建立變數時,若已賦值則通常不會在變數後面加型別 如果只有宣告而未賦值,則可加入型別 - number 沒有分整數或浮點數,就是number - string 可以用'', "", \`\`來包 - boolean 沒有truthy跟falsy > [name=Layors] from 2022/10/12 - object 可以只標示object,告訴TS這是某個object 或是可以標示整個完整的object,告訴TS裡面有什麼property ```typescript= const person: { name: string; age: number; } = { name: 'me', age: 23 } ``` - Array Array裡的型別可以鬆散也可以嚴謹 ```typescript= let activity: string[];//裡面只能存string let activity: any[];//裡面可以存各種型別 ``` - Tuple 固定長度、固定型別的Array ```typescript= let lottery: [number, number];//裡面只能有兩個數字,不能少也不能多 lottery = [1, 2, 3];//會報錯 lottery = [1, 2]; lottery.push(3);//用push的話TS偵測不到錯誤 ``` - Enums enumerate 列舉 ```typescript= enum Role { ADMIN, READ_ONLY, AUTHOR, } console.log(Role.ADMIN);//0 console.log(Role.READ_ONLY);//1 console.log(Role.AUTHOR);//2 ``` 數字可以自己更改 ```typescript= enum Role { ADMIN = 5, READ_ONLY, //6 AUTHOR, //7 } ... enum Role { ADMIN = 5, READ_ONLY = 7, AUTHOR = 9, } ``` 也可以不用數字,使用自訂的值 ```typescript= enum Role { ADMIN = 'me', READ_ONLY = 'you', AUTHOR = 'him', } ``` - any 不受限制 ### 進階type - union 讓一個變數可以接受多種型別 但有可能會報錯 所以可能會要搭配runtime check的typeof ```typescript= function combine(input1: number | string, input2: number | string) { let result; if (typeof input1 === "number" && typeof input2 === "number") { result = input1 + input2; } else { result = input1.toString() + input2.toString(); } return result; } ``` - literal type 特定形式的基本型別 ```typescript= //物種只能傳入cat或dog,而這兩個都是string,只是被限制只能用這兩個string function animal(name: string, species: 'cat' | 'dog') ``` - type aliases/ custom type 自訂型別 ```typescript=typescript= type combinable = number | string; //可接受number跟string type twoSpecies = 'cat' | 'dog'; //literal type type User = { name: string; age: number; }; //object,先定義好有助於減少不必要的重複 ... const user1: User = { name: 'Me', age: 23 }; ``` - function return type 意思就是return的值的type ```typescript= function add(n1: number, n2: number): number { return n1 + n2; }//return type加在function參數括號後面 function printIt(n1: number) { console.log("It is " + n1); }//沒有return任何東西,return type為void ``` 假如我們console.log沒有return任何值的function,會得到undefined,同時undefined也是TS的其中一個型別 但要有return東西的function才能把return type設成undefined 所以沒有return任何東西的function要用void - function JS可以把function存在一個變數裡,為了怕他被其他不function的值存進去,所以有function type ```typescript= let combine: Function;// F要大寫 combine = add; combine = printIt; ``` 但依然會被其他function蓋掉,所以可以更進一步的指定傳入的值、輸出的值的型別 ```typescript= let combine: (a: number, b: number) => number;// 用箭頭函式 ``` - callback ```typescript= function addAndPrint(n1: number, n2: number, cb: (num: number) => void) //這裡的void代表這個cb不會對傳來的值做任何使用 //即使用到的cb有傳值過來也不會報錯 ... addAndPrint(10, 20, (num) => { //這裡的num不用加type是因為在上面已經定義過這裡的num會是什麼型別了 console.log(num); }); ``` - unknown 跟any很像,但unknown type的變數不可以被賦予給其他型別的變數 可以說是嚴格一點的any 可以用typeof來確認unknown變數的type,再把他做處理 - never 另一種return type ```typescript= function newError(message: string, code: number): never { throw { message: message, errorCode: code }; } ``` 因為throw error,所以他永遠不會回傳東西,因此用never ## TypeScript Compiler ### 存檔後立即編譯 - 單一檔案 watch mode ```bash= tsc app.ts --watch tsc app.ts -w ``` 進入這個模式可以讓你每次存檔TS檔之後就自動編譯 但只能對一個檔案使用 - 整個project 進到該project的目錄之後執行下面的程式碼 ```bash= tsc -init ``` 執行完會建立tsconfig.json檔 此時輸入tsc便可編譯整個project裡的ts檔 此時可以搭配watch mode ```bash= tsc --watch tsc -w ``` > [name=Layors] from 2022/10/13 ### setting tsconfig.json告訴了typescript如何編譯ts檔 在最外面的大括號下面可以加上額外的設定 - exclude 用array來設定,可以用特定的檔案路徑,也可以選定特定的後綴 被選定的檔案或資料夾將不會被編譯 ```json= { { ... ... ... }, "exclude": [ "example.ts", //目前路徑除外的檔案 "testFolder", //除外的資料夾 "*.dev.ts", //目前路徑任何後綴為.dev.ts的檔案會被除外 "**/*.dev.ts", //任何資料夾底下的後綴為.dev.ts的檔案都會被除外 ] } ``` node_modules資料夾通常會被預設為除外,但在tsconfig.json裡不會被顯示出來 但如果你在tsconfig.json有加上exclude的設定,必須要把它放進去 不然typescript會變成不會把它除外 - include 如果加上這個設定,則變成只有在該array裡的檔案或資料夾才會被編譯 ```json= { { ... ... ... }, "include": [ "app.ts", "testFolder" ] } ``` - file 跟include很像,但他只能指定特定檔案,不能指定資料夾 ```json= { { ... ... ... }, "file": [ "app.ts", ] } ``` ### compilerOptions 這是在tsconfig.json裡的設定選項 - target 決定TS會被編譯成JS的哪個版本,ex:es5, es6, es2016....(es6為es2015) - allowjs, checkjs allowjs為true的話會讓js檔可以被ts編譯 checkjs為true的話js檔不會被ts編譯,但會檢查語法是否正常 - sourceMap 按F12並切換到source,可以看到我們的檔案,但只會看到js檔,而非ts檔 當sourceMap為true時,編譯後會產生app.js.map檔 在F12 source裡可以看到ts檔,也可在那裡設置斷點 - rootDir, outDir 一般來說,ts編譯完的js檔會被放在同一目錄下 而設置outDir可以指定編譯完的js檔該放在哪 而他也會複製資料夾結構 ```json= "outDir": "./jsFile", ``` ![](https://i.imgur.com/aV9L2IH.png) 但如上所示,outDir裡並沒有src資料夾 因為我們並沒有設置rootDir,因此他自動把src當根目錄 如果我們在外面又新增新的ts檔,他會重新選最外面的路徑為rootDir 因此一開始要設定好 ```json= "rootDir": "./", ``` rootDir必須包含所有的ts檔,不然會報錯 - removeComment js檔裡將不會有註解 - noEmit, noEmitOnError noEmit為true時不會產出js檔 noEmitOnError為true時在compile失敗時將不會產出js檔 - strict 在compilerOptions裡有一個Type Checking的區塊 如果strict設為true,則下面到alwaysStrict的選項就會被設為全開 如果沒有設為true,則要一個一個個別調整 - noUnusedLocal 若設為true,如果有未使用到的local變數,會報錯 - noImplicitReturns 若設為true,如果有function不一定會return值回來,會報錯 ## class ### constructor ```typescript= class Department { name: string; constructor(n: string) { this.name = n; } } const development = new Department("development"); ``` ### method ```typescript= describe(){ console.log('Department: ' + this.name); } ``` ### access modifier private, public, protected public是預設的 我們不希望一些資料暴露給別人,因此會用private private可以用在property跟method上 ```typescript= class Department { name: string; private employees: string[] = []; constructor(n: string) { this.name = n; } addEmployee(employee: string) { this.employees.push(employee); } } const development = new Department("development"); development.addEmployee("a"); development.addEmployee("b"); development.addEmployee("c"); empployees[0] = 'me'//若要嘗試修改private的東西會報錯 ``` ### shortcut for initialization ```typescript= class person { private name: string; private age: number; private job: string; constructor(n: string, age: number, job: string) { this.name = n; this.age = age; this.job = job; } } ``` 太繁瑣了,因此有簡化的方式: ```typescript= class person { constructor(private name: string, private age: number, public job: string) { } } ``` 在每個變數的前面加上access modifier,他就會自動幫你產生跟變數名字一樣的property ### readonly 如果你一些property不想之後被更改,可以在access modifier後面加上readonly ```typescript= private readonly name: string; ``` ### inheritance ```typescript= class Department { private employees: string[] = [] constructor(private id: string, private name: string) { } describe(){ console.log('Department: ' + this.name); } } class ITDepartment extends Department { constructor(id: string, private admins: string[]) { super(id, 'IT'); } } ``` 在這裡,ITDepartment可以使用describe,因為他是public method 而他無法使用到employees,因為該property是private 使用private的話就只能在那個class裡使用,就算是繼承過來的也無法使用 此時可以改用protected,可以讓繼承的class用到該property的同時也保持不被外部更動 ### getter, setter ```typescript= class Department { private employees: string[] = ['john', 'jane'] get employeeList(){ return this.employees; } set employeeList(name: string[]) { this.employees = name; } constructor(private id: string, private name: string) { } } const development = new Department("development"); console.log(development.employeeList) development.employeeList = ["me"]; console.log(development.employeeList); ``` - getter 裡面一定要有return 當呼叫getter的時候後面不用加() 通常用在你想要對傳回去的資料做一些處理的時候 - setter 括號裡面一定要有參數 當呼叫setter時一樣不用加(),用類似把值指定給變數的方式使用 > [name=Layors] from 2022/10/14 ### static method & property 不必新建一個該class的object就能從class裡呼叫 用法: 在method或property前面加上static ```typescript= class Department { //static property static establishedAt: number = 2022; //static method static createEmployee(name: string){ return {name: name}; } constructor(private id: string, private name: string) { //不能用this,下面這段code會報錯 console.log(this.establishedAt); //要用class的名字來呼叫 console.log(Department.establishedAt); } } console.log(Department.establishedAt); const newEmployee = Department.createEmployee("me"); ``` 在class裡面是不能用this來呼叫static property跟static method的 ### abstract class 當被繼承的class的method, property沒辦法提供一個default,希望繼承你的class都強制要override他時,用abstact 此時class跟該method, property前面都要加上abstract,method後面不加上{},property不賦予初始值 ```typescript= abstract class Department { abstract target: string; constructor(private id: string, private name: string) { } abstract describe(); } ``` 當class為抽象時,無法建立該class的object ### private constructor & singleton pattern 當你確定用這個class建立出來的object只會有一個的時候,會用到singleton pattern 用法為在constructor前面加上private,這樣做之後你就沒辦法用new建立該class的object 如何跟他互動? ->static method ```typescript= class JohnDoe { //用來儲存這個object private static instance: JohnDoe; private constructor() { } static getInstance() { if (JohnDoe.instance) { //回傳這個object return this.instance; } else { // 若還沒建立則new一個JohnDoe,並回傳這個object this.instance = new JohnDoe(); return this.instance; } } } //下面這段code會報錯 const John = new JohnDoe() ``` 上面可以用this跟static property互動的原因是因為我們在static method裡 若在constructor裡一樣不能用this,要用class的名字來呼叫 以上面code為例,如果我們要呼叫JohnDoe,直接用John.getInstance()即可 ## interface interface用來形容一個object看起來應該要是怎麼樣 interface只存在於TypeScript,JavaScript沒有interface interface不能給值給任何一個變數 ```typescript= interface Person { name: string; age: number; talk(phrase: string): void; } ``` 但其實custom type可以做到一樣的事 ```typescript= type Person = { name: string; age: number; talk(phrase: string): void; } ``` interface只能用於形容一個object應該長怎樣,type則較為彈性 interface的重要點在於,他告訴了那個class一定要遵守他的規定 ### class using interface ```typescript= interface Talkable { name: string; talk(phrase: string): void; } class Person implements Talkable { name: string; age = 30; //可以多加interface裡沒形容的property constructor(name: string) { this.name = name; } talk(words: string) { console.log(words); } } ``` - access modifier interface裡不能使用private, public或protected 但可以使用readonly 在interface裡使用之後不用再去class裡再次宣告,一樣會有效果 - 多重interface 一個class可以使用多個interface ```typescript= interface Named { name: string; } interface Talkable { talk(phrase: string): void; } class Person implements Named, Talkable { name: string; age = 30; constructor(name: string) { this.name = name; } talk(words: string) { console.log(words); } } ``` - interface extends interface ```typescript= interface Named { name: string; } //現在如果有人要用這個interface的話,必須要有name跟talk() interface Talkable extends Named{ talk(phrase: string): void; } ``` 而interface一樣可以extend多個interface - optional parameters interface可以讓某些property跟method變成不是一定要實做出來的 ```typescript= interface Named { name: string; age?: number; greet?('words'): void; } ``` 使用的方法是加上問號 property在變數名稱後面加問號 method在變數名稱跟括號中間加問號 這個方法在class裡也可以使用 ```typescript= interface Named { name: string; age?: number; greet?('words'): void; } class Person implements Named{ name: string; age?: number; constructor(name: string, job?: string) { this.name = name; } } ``` - 補充:用interface做function ```typescript= interface addFunction { (a: number, b: number): number; } let add: addFunction; add = (n1, n2) => { return n1 + n2; }; ``` ## Advanced types ### Intersection 把兩個custom type結合在一起 ```typescript= type Admin = { name: string; privileges: string[]; }; type Employee = { name: string; startDate: Date; }; type elevatedEmployee = Admin & Employee; const me: elevatedEmployee = { name: "L", privileges: ["create-server"], startDate: new Date(), }; ``` 基本上跟extends interfaces差不多 如果對兩個union type做intersection的話,會有跟使用and邏輯類似的結果 ```typescript= type a = string | boolean; type b = number | string; type c = a & b;//string //補充 type d = string & number;//never ``` ### Type Guard 基本上type guard就是各種確認型別的方法 - 基本型別 ```typescript= type combinable = number | string; function add(n1: combinable, n2: combinable) { if (typeof n1 === "string" || typeof n2 === "string") { return n1.toString() + n2.toString(); } //如果程式跑到這邊,那n1跟n2就一定都是number return n1 + n2; } ``` 上面所寫的是基本型別的type guard,但如果遇到了自訂型別會發生一些狀況 - 自訂型別 ```typescript= type Admin = { name: string; privileges: string[]; }; type Employee = { name: string; startDate: Date; }; type unknownEmployee = Admin | Employee; function printEmployeeInfo(emp: unknownEmployee) { console.log(emp.name); if (typeof emp === "Admin") { //TS會報錯 console.log(emp.privileges); } } ``` 因為程式碼最終要編譯回JS,而JS並不懂我們的自訂型別,因此無法用typeof來確認一個變數是否為自訂型別 因此,我們要用另一個方法:**in** ```typescript= function printEmployeeInfo(emp: unknownEmployee) { console.log(emp.name); if ("privileges" in emp) { //這樣就會過了 console.log(emp.privileges); } } ``` - class 使用的是instanceof來確認你是哪個class 這是JS原生的功能 ```typescript= class car { drive() { console.log("driving..."); } } class truck { drive() { console.log("so heavy"); } loadCargo() { console.log("loading..."); } } type vehicle = car | truck; const v1 = new car(); const v2 = new truck(); function useVehicle(vehicle: vehicle) { vehicle.drive(); if (vehicle instanceof truck) { vehicle.loadCargo(); } } ``` 但如果有用到interface,那這方法會失效,因為這是runtime check,而interface的部分不會被編譯成JS ### Discriminated Union 一種特殊的type guard,讓你對union type用type guard時可以簡單一些 有對class的type guard,但如果是用interface就無法使用 因此有一種方法,就是在所有的interface裡加上一樣的property 並且使用switch ```typescript= interface Bird { type: "bird"; flyingSpeed: number; } interface Horse { type: "horse"; runningSpeed: number; } type Animal = Bird | Horse; function moveAnimal(animal: Animal) { let speed: number; switch (animal.type) { case "bird": speed = animal.flyingSpeed; break; case "horse": speed = animal.runningSpeed; } console.log("speed: " + speed); } ``` > [name=Layors] from 2022/10/17 ### Type Casting type casting是指你自己幫助TypeScript去分辨他分辨不了的type 例如:操作DOM ```typescript= //TypeScript不知道放在pass裡的東西到底是什麼,他只知道這是個HTML element const pass = document.getElementById("pass")!; ``` 因為TypeScript不知道你是不是會拿到東西,在這個情況下pass可能是null,在後面加!等於告訴TypeScript他抓到的東西永遠不會是null 有兩種解決方法 1. ```typescript= const pass = <HTMLInputElement>document.getElementById("pass")!; ``` 在前面加上<>,並在裡面寫明這是什麼東西 這個有點像是react的方法 2. ```typescript= const pass = document.getElementById("pass")! as HTMLInputElement; ``` ### Index ```typescript= interface ErrorContainer{ email: string [Prop: string]: string; } const err: ErrorContainer = { email : 'invalid email', name: 'invalid name' } ``` 上面的code意思是這個object的key一定要是string,而key裡存的值也是string email: string的意思是你先預設了這個object裡一定會有一個叫email的key 之後在建立這個物件的時候就一定要寫出他,不然會報錯 ### Function Overload ```typescript= type combinable = number | string; function add(n1: number, n2: number): number function add(n1: combinable, n2: combinable) { if (typeof n1 === "string" || typeof n2 === "string") { return n1.toString() + n2.toString(); } return n1 + n2; } ``` 根據變數的型別、數量不同,做出不同類型的return,但function名字都一樣 假如沒做function overload,使用add function的結果都會被TypeScript識別為combinable 而有做的話,可以被識別為各種型別 !!!但要注意,一旦做了function overload,下面function的implementation會變得無法呼叫,你只能去呼叫已經宣告的function overload 像上面的例子,如果我們把兩個string丟進add裡,TypeScript會報錯,因為你只有寫出當兩個變數都是number時會return什麼,還沒寫出丟兩個string進去會發生什麼 ```typescript= function add(n: number): number function add(n1: number, n2: number): number function add(n1: number, n2?: number) { if(n2 !== undefined){ return n1 + n2; } return n1; } ``` ### optional chaining 當你在別的地方(ex:database)取得了一個object,但你不知道這個object裡的某個property是否存在,Typescript不會報錯,因為他偵測不到 這時可以用option chining ```typescript= type JobData = { title: string; desc: string; }; type Person = { name: string; age?: number; job?: JobData; }; const John: Person = { name: "John", // age: 23, // job: { title: "paladin", desc: "May light blesses you" }, }; console.log(John?.age); console.log(John?.job?.title); ``` 先定義好取出來的資料的結構,可能會沒define到的property加上問號 之後要用到該property時,在該object的後面加上問號,如果該property不存在,不會產生runtime error而跳出程式 ### nullsh coalescing ```typescript= const input = ""; const stored = input || "default value"; console.log(stored); ``` 如果你有一個input你不知道他是null, undefined或是有東西,用上面的程式可以讓你只在input有值的時候做操作,但如果是空字串或是數字0,這也會被歸類在falsy value 但如果我們想要保存空白input或數字0,要用另一種方式:?? ```typescript= const input = ""; const stored = input ?? "default value"; console.log(stored); ``` **NEXT** > [TYPESCRIPT](/-u29b5iuS8GMMwrlaCirEQ)