ts 是什麼
TS是TypeScript的縮寫,由微軟開發的一種開源的程式語言
以前官網說“ts 是 js 超級”,現在改為: TypeScript是具有型別語法的JavaScript。
目前 TypeScript 5.4 已經發布(2024-03) —— ts 官網
Tip:ts缺點:開發更費麻煩,要多寫東西了,看個人取捨。
環境
基於筆者博文《vue3 入門》,就像這樣:
<template> <section> </section> </template> <script lang="ts" setup name="App"> // ts </script> <style> </style>
也可以直接在ts線上執行環境進行。
推導型別和顯示註解型別
TS = 型別
+ javascript
ts 編譯過程:
- TypeScript原始碼 -> TypeScript AST
型別檢查器
檢查AST- TypeScript AST -> JavaScript 原始碼
顯示註解型別,語法:value:type
告訴型別檢查器,這個 value 型別是 type。請看示例:
<template> <p>{{ a }}</p> <p>{{ b }}</p> <p>{{ c }}</p> </template> <script lang="ts" setup name="App"> // 顯示註解型別 let a: number = 1 // a 是數字 let b: string = 'hello' // b 是字串 let c: boolean[] = [true, false]; // 布林型別陣列 </script>
如果將 a 寫成 let a: number = '3'
,vscode 中 a 就會出現紅色波浪,移上去會看到提示:不能將型別“string”分配給型別“number”。
如果想讓 typescript 推到型別,就去掉註解,讓 ts 自動推導。就像這樣:
// 推導型別 let a = 1 // a 是數字 let b = 'hello' // b 是字串 let c = [true, false]; // 布林型別陣列
去掉註解後,型別並沒有變。並且如果嘗試修改 a 的型別,ts 也會報錯。就像這樣:
let a = 1 // a 是數字 // 嘗試替換成字串,vscode 會提示:不能將型別“boolean”分配給型別“number”。 a = true
Tip:有人說“最好讓 ts 推導型別,少數情況才需要顯示註解型別”。
另外雖然大量錯誤 ts 在編譯時無法捕獲,例如堆疊溢位、網路斷連,這些屬於執行時異常。ts 能做的是將 js 執行時的報錯提到編譯時。比如以下程式碼:
const obj = { width: 10, height: 15 }; // 提示 heigth 屬性寫錯了 const area = obj.width * obj.heigth; let a = 1 + 2 let b = a + 3 // 滑鼠以上 c,可以看到 c 對應的型別 let c = { apple: a, banana: b }
型別斷言
請看示例:
let arr = [1, 2, 3] // r 為3 const r = arr.find(item => item > 2) // “r”可能為“未定義”。ts(18048) // const r: number | undefined r * 5 // {1}
行 {1} 處的 r 會錯誤提示,說 r 可能會是 undefined。
要解決這個問題,可以使用型別斷言
:用來告訴編譯器一個值的具體型別,當開發者比編譯器更瞭解某個值的具體型別時,可以使用型別斷言來告訴編譯器應該將該值視為特定的型別。
型別斷言有兩種形式,分別是尖括號語法
和 as 語法
。這裡說一下 as 語法:value as type
。請看示例:
let someValue: any = "this is a string"; let strLength: number = (someValue as string).length;
上述示例改成這樣,r 就不會報錯了。
// 告訴編譯器,r 一定是一個 number const r = arr.find(item => item > 2) as number r * 5
Tip:由於需要人為干預,所以使用起來要謹慎
基礎型別
js 基本型別大概有這些:
let num1 = 10; let str1 = 'Hello'; let isTrue = true; let undefinedVar; let nullVar = null; const symbol1 = Symbol('description'); const bigIntNum = 9007199254740991n; let notANumber = NaN; let infinite = Infinity; console.log(typeof num1); // number console.log(typeof str1,); // string console.log(typeof isTrue); // boolean console.log(typeof undefinedVar); // undefined console.log(typeof nullVar); // object console.log(typeof symbol1); // symbol console.log(typeof bigIntNum); // bigint console.log(typeof notANumber); // number console.log(typeof infinite); // number
ts 中基本型別有:
// let v1: String = 'a' - 大寫 String 也可以 let v1: string = 'a' let v2: number = 1 let v3: boolean = true let v4: null = null let v5: undefined = undefined // 字串或者null let v6: string | null = null // 錯誤:不能將型別“5”分配給型別“1 | 2 | 3” let v7: 1 | 2 | 3 = 5 // 正確 let v8: 1 | 2 | 3 = 2
聯合型別
陣列
ts 陣列有兩種方法,看個人喜好即可。請看示例:
// 方式一 // 定義一個由數字組成的陣列 let arr1: number[] = [2, 3, 4] // 報錯:不能將型別“string”分配給型別“number” let arr2: number[] = [2, 3, 4, ''] // 方式二 let arr3: Array<string> = ['a', 'b', 'c'] // 報錯:不能將型別“number”分配給型別“string”。 let arr4: Array<string> = ['a', 'b', 'c', 4]
元組
在 TypeScript 中,元組(Tuple)是一種特殊的陣列型別,它允許您指定一個固定長度和對應型別的陣列
let arr5:[string, number, string] = ['a', 1, 'b'] // 報錯:不能將型別“[string, number]”分配給型別“[string, number, string]”。源具有 2 個元素,但目標需要 3 個。 let arr6:[string, number, string] = ['a', 1] // 正確 arr6[0] = 'a2' // 錯誤:不能將型別“number”分配給型別“string”。 arr6[0] = 1 // 第三個新增 ? 表明可選,這樣只傳入 2 個數也不會報錯 let arr7:[string, number, string?] = ['a', 1, 'b']
列舉
列舉需要使用關鍵字 enum。請看示例:
// 就像定義物件,不過不需要 = enum TestEnum { a, b, c, } // 1 console.log(TestEnum.b); // b console.log(TestEnum[1]); // string console.log(typeof TestEnum[1]);
ts 可以自動為列舉型別中的各成員推導對應數字。上面示例推導結果:
enum TestEnum { a = 0, b = 1, c = 2, }
也可以自己手動設定:
enum TestEnum2 { a = 3, b = 13, c = 23, } // 13 console.log(TestEnum2.b);
比如這個,c 就是 b 的下一個數字:
enum TestEnum3 { a, b = 13, c, } // 14 console.log(TestEnum3.c);
使用場景
:比如你之前根據訂單狀態寫了如下程式碼,可以用列舉來增加可讀性。
if(obj.state === 0){ }else if(obj.state === 1){ }else if(obj.state === 2){ }else if(obj.state === 3){ }
// 最佳化後 enum 訂單狀態{ 取消, 上線, 傳送, 退回, ... } if(obj.state === 訂單狀態.取消){ }else if(obj.state === 訂單狀態.上線){ }else if(obj.state === 訂單狀態.傳送){ }else if(obj.state === 訂單狀態.退回){ }
函式
定義一個函式,引數報錯:
// 引數 a 和 b報錯。例如:a - 引數“a”隱式具有“any”型別。 function fn1(a, b){ return a + b }
定義引數型別:
function fn2(a: number, b : number){ return a + b }
定義引數 b 可選,返回值是 number型別。請看示例:
// b是可選。 // 必選的放左側,可選的放後側 function fn5(a: number, b?: number): number{ return 10 } // 應有 1-2 個引數,但獲得 0 個。 fn5()
定義引數 a 的預設值,rest是一個字串陣列:
// a 有一個預設值 10 function fn7(a = 10, b?: number, ...rest:string[]): number{ return 10 } fn7(1,2, 'a', 'b')
void
通常用於函式,表示沒有 return 的函式。
function fn3(a: number, b : number):void{ // 不能將型別“number”分配給型別“void”。 return a + b } function fn4(a: number, b : number): void{ }
介面
通常用於物件的定義。請看示例:
interface Person{ name: string, age: number } const p: Person = { name: 'peng', age: 18 } // 報錯:型別 "{ name: string; }" 中缺少屬性 "age",但型別 "Person" 中需要該屬性。ts(2741) const p2: Person = { name: 'peng', }
型別別名
比如定義了一個變數 v1,其型別可以是 number 或 string,但是好多地方都是這個型別:
let v1: number | string = 3
我們可以透過 type
定義一個別名
。就像這樣:
// 定義別名 Message type Message = number | string let v2: Message = 'hello' // 報錯:不能將型別“boolean”分配給型別“Message” let v3: Message = true
泛型
比如定義如下一個處理 number 的函式:
function fn1(a: number, b:number): number[]{ return [a, b] }
假如以後想把這個函式作為一個通用函式,除了可以處理 number,還可以處理 string 等其他型別,比如:
function fn1(a: string, b:string): string[]{ return [a, b] }
a: string | number
又交叉了。就像這樣:
function fn1(a: string | number, b:string | number): string[]{ return [a, b] }
這裡可以使用泛型
,請看示例:
// 定義一個變數,比如 T function fn1<T>(a: T, b:T): T[]{ return [a, b] } fn1<number>(11, 11) fn1<string>('a', 'a') // 正確,ts 會自動推導 fn1('a', 'a')
再看一個泛型示例:
// 引數 arr 是 T 型別的陣列 // 返回 T 型別或 undefined function firstElement<T>(arr: T[]): T | undefined { return arr[0]; } firstElement(['a', 'b'])
函式過載
java 中函式過載是定義多個方法,呼叫時根據引數型別
和數量
的不同執行不同的方法。例如下面定義兩個 add:
// 方法過載示例:兩個引數的相加 public int add(int a, int b) { return a + b; } // 方法過載示例:三個引數的相加 public int add(int a, int b, int c) { return a + b + c; }
ts 這裡過載和 java 中的有些不同,可以稱之為函式過載申明
。
比如首先我們寫了一個數字相加
或字串相加
的方法:
// 數字相加 // 字串相加 function combine(x: number | string, y: number | string): number | string { if (typeof x === 'number' && typeof y === 'number') { return x + y; } else if (typeof x === 'string' && typeof y === 'string') { return x + y; } // 處理其他情況 return 'Invalid input'; } console.log(combine(1, 2)); // 輸出:3 console.log(combine('hello', 'world')); // 輸出:helloworld
這裡有兩個問題:
// 問題一:滑鼠移動到 combine 顯示: // function combine(x: number | string, y: number | string): number | string console.log(combine(1, 2)); console.log(combine('hello', 'world')); // 問題二:傳入 number和 string 不合法,但不報錯。滑鼠移動到 combine 顯示: // function combine(x: number | string, y: number | string): number | string console.log(combine(1, 'two')); // 輸出:Invalid input
現在加上函式過載申明
,就能解決上述兩個問題。請看示例:
// 函式過載 function combine(x: number, y: number): number; // 變數名可以不是x、y function combine(x2: string, y2: string): string; function combine(x: number | string, y: number | string): number | string { // 不變 } // function combine(x: number, y: number): number (+1 overload) console.log(combine(1, 2)); // function combine(x: string, y: string): string (+1 overload) console.log(combine('hello', 'world')); // 報錯:沒有與此呼叫匹配的過載。 // 第 1 個過載(共 2 個),“(x: number, y: number): number”,出現以下錯誤。 // 第 2 個過載(共 2 個),“(x: string, y: string): string”,出現以下錯誤。ts(2769) console.log(combine(1, 'two'))
介面繼承
直接看示例:
interface Person{ name: string, age: number } // Student 繼承 Person interface Student extends Person{ school: string } // 提示p缺少3個屬性 // 型別“{}”缺少型別“Student”中的以下屬性: school, name, agets(2739) const p: Student = { }
Student 繼承 Person,有了3個屬性。
類的修飾符
類的修飾符有:public、private、protected、static、readonly...。用法請看下文:
比如有這樣一段正常的js程式碼:
class People{ constructor(name){ this.name =name; } // 不需要逗號 sayName(){ console.log(this.name) } } let people = new People('aaron') people.sayName() // aaron
放在 ts(比如 ts線上執行環境) 中會報錯如下:
Parameter 'name' implicitly has an 'any' type. Property 'name' does not exist on type 'People'. Property 'name' does not exist on type 'People'.
需要修改如下兩處即可消除所有錯誤:
class People{ - constructor(name){ + // 消除ts報錯:型別“People”上不存在屬性“name” + name: string + constructor(name: string){ this.name =name; } // 不需要逗號
其中 name: string
的作用:宣告 People 類有個必填屬性。例項化 People 類的時候,必須傳入一個 string 型別的 name 屬性。
接著加一個可選屬性 age:
// 透過?將 age 改成可選。解決:屬性“age”沒有初始化表示式,且未在建構函式中明確賦值。 age?: number
可以設定預設值:
// 根據預設值推斷型別,而且是必選屬性 money = 100
Tip:稍後我們會看到對應的 js 是什麼樣子。
屬性預設是 public
,自身可以用,繼承的子類中也可以使用。public 還可以這麼寫,效果和上例等價:
constructor(name){ - name: string - constructor(name: string){ + constructor(public name: string){ this.name =name; }
另外還有 private
表明只能在類中使用。protected
只能在類和子類中使用。請看示例:
class People{ ... // 屬性預設是 public,自身可以用、繼承也能用 public money2 = 200 private money3 = 300 protected money4 = 400 constructor(name: string){ this.name =name; } sayName(){ console.log(this.name) } } let people = new People('aaron') console.log(people.money); // 屬性“money3”為私有屬性,只能在類“People”中訪問。ts(2341) console.log('people.money3: ', people.money3); // 300 // 屬性“money4”受保護,只能在類“People”及其子類中訪問。ts(2445) console.log('people.money4: ', people.money4); // 400
注
:雖然 vscode 報錯,但瀏覽器控制檯還是輸出了。或許 ts 只是靜態編譯,對應的js 沒有做特殊處理
,比如 private 宣告 money4,實際上並沒有實現。請看ts線上執行環境 ts 對應的 js:
// ts class People{ name: string age?: number money = 100 public money2 = 200 private money3 = 300 protected money4 = 400 constructor(name: string){ this.name =name; } sayName(){ console.log(this.name) } } let people = new People('aaron') console.log('people.money3: ', people.money3); console.log('people.money4: ', people.money4);
// 對應的js "use strict"; class People { constructor(name) { this.money = 100; this.money2 = 200; this.money3 = 300; this.money4 = 400; this.name = name; } sayName() { console.log(this.name); } } let people = new People('aaron'); console.log('people.money3: ', people.money3); console.log('people.money4: ', people.money4);
js 中靜態屬性
使用如下:
protected money4 = 400 // 靜態屬性 + static flag = 110 console.log('People.flag: ', People.flag);
例如將靜態屬性設定成私有,只能在類中使用。請看示例:
// 靜態屬性 private static flag = 110 // 報錯:屬性“flag”為私有屬性,只能在類“People”中訪問。 console.log('People.flag: ', People.flag);
多個修飾符
可以一起使用,但有時候需要注意順序,vscode 也會給出提示。就像這樣:
// “static”修飾符必須位於“readonly”修飾符之前。ts(1029) readonly static flag = 110
比如定義一個靜態只讀屬性:
static readonly flag2 = 110 // 報錯:無法為“flag2”賦值,因為它是隻讀屬性。ts(2540) People.flag2 = 111
類的存取器
感覺就是 js 的 get 和 set。比如下面就是一個 js 的get、set示例:
class People { constructor(name) { this.name = name; } get name() { return 'apple'; } set name(v) { console.log('set', v); } } let people = new People('aaron') // set aaron people.name = 'jia' // set jia console.log(people.name); // apple
對應 ts 中的存取器就是這樣:
class People{ constructor(name: string){ this.name = name; } get name(){ return 'apple' } set name(v){ console.log('set', v) } } let people = new People('aaron') // set aaron people.name = 'jia' // set jia console.log(people.name); // apple
注:這個例子很可能會棧溢位,就像這樣:
class People{ constructor(name: string){ this.name = name; } get name(){ return 'apple' } set name(v){ console.log('v: ', v); // 棧溢位 // 報錯:VM47:10 Uncaught RangeError: Maximum call stack size exceeded this.name = v } } let people = new People('aaron')
所以可以這麼寫:
class People { private _name: string = '' get name(): string{ return 'peng' } set name(val: string){ this._name = val } } let people = new People() people.name // 報錯:屬性“_name”為私有屬性,只能在類“People”中訪問。ts(2341) people._name
不寫型別,ts 也會自動推導,比如去除型別後也可以。就像這樣:
// 自動推導型別 class People { private _name = 'peng' get name(){ return 'peng' } set name(val){ this._name = val } } let people = new People()
抽象類
抽象類(abstract),不允許被例項化,抽象屬性和抽象方法必須被子類實現
。更像一個規範。請看示例
abstract class People { // 可以有抽象屬性和方法 abstract name: string abstract eat(): void // 也可以有普通屬性和方法 say() { console.log('hello: ' + this.name) } } // 如果不實現 name 和 eat 方法則報錯 class Student extends People{ name: string = '學生' // 既然沒報錯 - 抽象類中返回是 void,這裡返回string eat(){ return 'eat apple' } } const s1 = new Student() s1.say() console.log(s1.eat()); // eat apple
抽象類定義了一個抽象屬性、一個抽象方法,一個具體方法
。子類必須實現抽象屬性和抽象方法,子類例項可以直接訪問抽象類中具體的方法。請看對應的 js 程式碼,你就能很明白。
class People { say() { console.log('hello: ' + this.name); } } class Student extends People { constructor() { super(...arguments); this.name = '學生'; } eat() { return 'eat apple'; } } const s1 = new Student(); s1.say(); console.log(s1.eat());
類實現介面
前面我們用介面定義了一個型別:
interface Person{ name: string, age: number } const p: Person = { name: 'peng', age: 18 }
抽象類如果只寫抽象方法和屬性,那麼就和介面很相同了。另外介面用 interface
關鍵字定義,子類可以實現 implements
(注意這個單詞是複數
) 多個介面(不能同時繼承多個)。請看示例:
interface People { name: string eat(): void } interface A{ age: number } // 實現兩個介面,所有屬性和方法都需要實現 class Student implements People, A{ name: string = '學生' age = 100 // 既然沒報錯 eat(){ return 'eat apple' } } const s1 = new Student() console.log(s1.eat()); // eat apple
泛型類
使用類時,除了可以使用介面來規範行為,還可以將類和泛型結合,稱為泛型類
。
比如現在 deal 是處理 string 的方法:
class People { value: string; constructor(value: string) { this.value = value; } deal(): string { return this.value; } } const p1 = new People('peng') p1.deal()
後面我需要 deal 又能處理 number,這樣就可以使用泛型。就像這樣:
class People<T> { value: T; constructor(value: T) { this.value = value; } deal(): T { return this.value; } } const p1 = new People('peng') p1.deal() const p2 = new People(18) p2.deal()
多個泛型寫法如下:
class Pair<T, U> { private first: T; private second: U; constructor(first: T, second: U) { this.first = first; this.second = second; } public getFirst(): T { return this.first; } public getSecond(): U { return this.second; } } // 使用帶有多個泛型型別引數的泛型類 let pair1 = new Pair<number, string>(1, "apple"); console.log(pair1.getFirst()); // 1 console.log(pair1.getSecond()); // apple let pair2 = new Pair<string, boolean>("banana", true); console.log(pair2.getFirst()); // banana console.log(pair2.getSecond()); // true
其他
Error Lens
:提供了一種更直觀的方式來展示程式碼中的問題,如錯誤、警告和建議,以幫助開發者更快速地識別和解決問題。
vscode 直接安裝後,會將紅色錯誤提示直接顯示出來,無需將滑鼠移到紅色波浪線才能看到錯誤提示。