前端 Typescript 入門

彭加李發表於2024-03-29

前端 Typescript 入門

Ant design vue4.x 基於 vue3,示例預設是 TypeScript。比如 table 元件管理。

vue3 官網介紹也使用了 TypeScript,例如:響應式 API:核心

華為的鴻蒙OS(HarmonyOS)開發中也可以使用 TypeScript

本篇目的用於對 TS 進行掃盲

Tipts 路線圖

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 直接安裝後,會將紅色錯誤提示直接顯示出來,無需將滑鼠移到紅色波浪線才能看到錯誤提示。

相關文章