typescript使用入門及react+ts實戰

ts介紹

TypeScript是一種由微軟開發的自由和開源的程式語言。它是 JavaScript 的一個超集,而且本質上向這個語言添加了可選的靜態類型和基於類的面向對象編程。

與js關係

image.png

ts與js區別

TypeScript JavaScript
JavaScript 的超集,用於解決大型項目的程式碼複雜性 一種腳本語言,用於創建動態網頁。
強類型,支援靜態和動態類型 動態弱類型語言
可以在編譯期間發現並糾正錯誤 只能在運行時發現錯誤
不允許改變變數的數據類型 變數可以被賦予不同類型的值

使用ts好處?

  • 規範我們的程式碼,編譯階段發現錯誤,在原生基礎上加了一層定義
  • ts是js的類型化超集,支援所有js,並在此基礎上添加靜態類型和面向對象思想
  • 靜態類型,定義之後就不能改變

ts缺點

  • 不能被瀏覽器理解,需要被編譯成 JS
  • 有一定的學習成本
  • 程式碼量增加

環境安裝

npm install -g typescript 
// yarn方式  
yarn global add typescript

// 檢查是否安裝成功
tsc -v

// 編譯ts文件
tsc index.ts

vscode中監聽ts文件自動編譯

// 1. 初始化tsconfig.json文件
tsc --init 

2. 左上角菜單點擊終端->運行任務->點擊typescript->選擇tsc監視tsconfig文件

基礎類型

boolean、number 和 string 類型

  • boolean
let isDone: boolean = false;
// let isDone: boolean = "123"; //Type 'string' is not assignable to type 'boolean'

賦值與定義的不一致,會報錯,靜態類型語言的優勢就體現出來了,可以幫助我們提前發現程式碼中的錯誤。

  • number
let decimal: number = 6;
  • string
let color: string = "blue";

數組類型

let list: number[] = [1, 2, 3];
// let list: Array<number> = [1, 2, 3];

// 數組裡的項寫錯類型會報錯
let list: number[] = [1, 2, "3"]; // error

// push 時類型對不上會報錯
list.push(4)
list.push("4") // error

如果數組想每一項放入不同類型數據,怎麼辦?那我們可以使用元組類型

元組類型

元組類型允許表示一個已知元素數量和類型的數組,各元素的類型不必相同。

let x: [string, number] = ["hello", 10];

// 寫錯類型會報錯
let x: [string, number] = [10, "hello"];  // Error

// 越界會報錯
let x: [string, number] = ["hello", 10, 110];  // Error

// 使用 push 時,不會有越界報錯,但是只能 push 定義的 number 或者 string 類型
x.push("hello2")
x.push(11)
console.log(x) //['hello', 10, 'hello2', 11]

// push 一個沒有定義的類型,報錯
x.push(true) // error

undefined 和 null 類型

let u:undefined = undefined  
let n:null = null

默認情況下 null 和 undefined 是所有類型的子類型。就是說你可以把 null 和 undefined 賦值給 number 類型的變數。

let age: number = null
let realName: string = undefined

但是如果指定了 –strictNullChecks 標記,null 和 undefined 只能賦值給 void 和它們各自,不然會報錯。

any、unknown 和 void 類型

  • any

不清楚用什麼類型,可以使用 any 類型

let notSure: any = 4
notSure = "maybe a string"     // 可以是 string 類型
notSure = false                // 也可以是 boolean 類型

notSure.name                   // 可以隨便調用屬性和方法
notSure.getName()

不過,不建議使用 any,不然就喪失了 TS 的意義。

  • unknown

不建議使用 any,當我不知道一個類型具體是什麼時,該怎麼辦?

可以使用 unknown 類型

把 param 定義為 any 類型,TS 就能編譯通過,沒有把潛在的風險暴露出來,萬一傳的不是 number 類型,不就沒有達到預期了嗎。

function divide(param: any) {
  return param / 2;
}

把 param 定義為 unknown 類型 ,TS 編譯器就能攔住潛在風險
因為不知道 param 的類型,使用運算符 /,導致報錯。

function divide(param: unknown) {
  return param / 2;   // error
}

配合類型斷言,可以解決這個問題

function divide(param: unknown) {
  return param as number / 2;
}
  • void

void類型與 any 類型相反,它表示沒有任何類型。
比如函數沒有明確返回值,默認返回 Void 類型

function welcome(): void {
  console.log('hello')
}

never 類型

never類型表示的是那些永不存在的值的類型。

有些情況下值會永不存在,比如,

  • 如果一個函數執行時拋出了異常,那麼這個函數永遠不存在返回值,因為拋出異常會直接中斷程式運行。
  • 函數中執行無限循環的程式碼,使得程式永遠無法運行到函數返回值那一步。
// 異常
function fn(msg: string): never { 
  throw new Error(msg)
}

// 死循環
function fn(): never { 
  while (true) {}
}

枚舉

  • 枚舉的意義在於,可以定義一些帶名字的常量集合,清晰地表達意圖和語義,更容易地理解程式碼和調試。
  • 常用於和後端聯調時,區分後端返回的一些代表狀態語義的數字或字元串,降低閱讀程式碼時的心智負擔。

基本使用

定義一個數字枚舉

enum Color {
  Red,
  Green,
  Blue,
}
console.log(Color.Red) // 0
console.log(Color.Green) // 1
console.log(Color.Blue) // 2
console.log(Color[0]) // Red

特點:

  • 數字遞增,枚舉成員會被賦值為從 0 開始遞增的數字
  • 反向映射,枚舉會對枚舉值到枚舉名進行反向映射,

如果枚舉第一個元素賦有初始值,就會從初始值開始遞增

enum Color {
  Red=3,
  Green,
  Blue,
}
console.log(Color.Red) // 3
console.log(Color.Green) // 4
console.log(Color.Blue) // 5

手動賦值

定義一個枚舉來管理外賣狀態,分別有已下單,配送中,已接收三個狀態。

enum Status {
  Buy = 1,
  Send,
  Receive
}
console.log(Status.Buy)      // 1
console.log(Status.Send)     // 2
console.log(Status.Receive)     // 3

但有時候後端給你返回的數據狀態是亂的,就需要我們手動賦值。
比如後端說 Buy 是 100,Send 是 20,Receive 是 1,就可以這麼寫:

enum Status {
  Buy = 100,
  Send = 20,
  Receive = 1
}
console.log(Status.Buy)      // 100
console.log(Status.Send)     // 20
console.log(Status.Receive)     // 1

計算成員

枚舉中的成員可以被計算

enum FileAccess {
  // constant members
  None,
  Read    = 1 << 1,
  Write   = 1 << 2,
  ReadWrite  = Read | Write,
  // computed member
  G = "123".length
}
console.log(FileAccess.None)       // 0
console.log(FileAccess.Read)       // 2   -> 010
console.log(FileAccess.Write)      // 4   -> 100
console.log(FileAccess.ReadWrite)  // 6   -> 110
console.log(FileAccess.G)       // 3

字元串枚舉

字元串枚舉意義在於,提供有具體語義的字元串,可以更容易地理解程式碼和調試。

enum Direction {
  Up = "UP",
  Down = "DOWN",
  Left = "LEFT",
  Right = "RIGHT",
}
const value = 'UP'
if (value === Direction.Up) {
    // do something
}

常量枚舉

常量枚舉編譯出來的 JS 程式碼會簡潔很多,提高了性能。常量枚舉不允許包含計算成員

const enum Direction {
  Up = "UP",
  Down = "DOWN",
  Left = "LEFT",
  Right = "RIGHT",
}
const value = 'UP'
if (value === Direction.Up) {
    // do something
}

會被編譯成:

const value = 'UP';
if (value === "UP" /* Up */) {
    // do something
}

不寫const會被編譯成:

(function (Direction) {
    Direction["Up"] = "UP";
    Direction["Down"] = "DOWN";
    Direction["Left"] = "LEFT";
    Direction["Right"] = "RIGHT";
})(Direction || (Direction = {}));
const value = 'UP';
if (value === Direction.Up) {
    // do something
}

這一堆定義枚舉的邏輯會在編譯階段會被刪除,常量枚舉成員在使用的地方被內聯進去。

枚舉類型

當枚舉作為類型時,表示該屬性只能為枚舉中的某一個成員

enum SEX{
    man = '男',
    woman = '女',
    unknown = '未知'
}
let arr:Array<SEX> = [SEX.man,SEX.woman]
let s:SEX = SEX.man
console.log(s); // 男

介面

在面向對象語言中,介面(Interfaces)是一個很重要的概念,它是對行為的抽象,而具體如何行動需要由類(classes)去實現(implement)。

在ts中,介面除了可用於對類的一部分行為進行抽象以外,也常用於對「對象的形狀(Shape)」進行描述。

介面一般首字母大寫

定義對象類型

定義對象類型,可以對對象形狀進行描述,介面和對象形狀必須保持一致

interface IntfPerson {
    name: string;
    age: number;
}

const jack: IntfPerson = {
    name: 'jack',
    age: 25
}

// 缺少或多出屬性都會報錯
const jack: IntfPerson = {
    name: 'jack'
}
const jack: IntfPerson = {
    name: 'jack',
    age: 25,
    city: '深圳'
}

可選屬性

如果不需要完全匹配形狀,可以使用可選屬性

interface IntfPerson {
    name: string;
    age?: number;
}
const jack: IntfPerson = {
    name: 'jack'
}

只讀屬性

有時候我們希望對象中的一些欄位只能在創建的時候被賦值,那麼可以用 readonly 定義只讀屬性

interface IntfPerson {
    readonly id: number;
    name: string;
    age?: number;
}

let jack: IntfPerson = {
    id: 89757,
    name: 'Tom',
};

// 報錯
jack.id = 9527;

描述函數類型

interface 也可以用來描述函數類型

interface ISum {
    (x:number,y:number):number
}

const add:ISum = (num1, num2) => {
    return num1 + num2
}
console.log(add(1,2)); // 3

es6中類的用法

先了解下es6中類的用法:

  • 使用 class 定義類,使用 constructor 定義構造函數
  • 通過 new 生成新實例的時候,會自動調用構造函數。
class Animal {
  public name;
  constructor(name:string) {
      this.name = name;
  }
  sayHi() {
      return `My name is ${this.name}`;
  }
}
let a = new Animal('Kerry');
console.log(a.sayHi()); // My name is Kerry
  1. 類的繼承
    使用 extends 關鍵字實現繼承,子類中使用 super 關鍵字來調用父類的構造函數和方法
class Cat extends Animal {
  constructor(name:string) {
    super(name); // 調用父類的 constructor(name)
    console.log(this.name);
  }
  sayHi() {
    return 'Meow, ' + super.sayHi(); // 調用父類的 sayHi()
  }
}

let c = new Cat('Tom'); // Tom
console.log(c.sayHi()); // Meow, My name is Tom
  1. 存取器
    使用 getter 和 setter 可以改變屬性的賦值和讀取行為
class Animal {
  constructor(name:string) {
    this.name = name;
  }
  get name() {
    return 'Kerry';
  }
  set name(value) {
    console.log('setter: ' + value);
  }
}

let a = new Animal('Kitty'); // setter: Kitty
a.name = 'Tom'; // setter: Tom
console.log(a.name); // Kerry
  1. 靜態方法
    使用 static 修飾符修飾的方法稱為靜態方法,它們不需要實例化,而是直接通過類來調用:
class Animal {
  static cry() {
    console.log('會哭'); // 會哭
  }
}
Animal.cry(); 

// 報錯
const animal = new Animal()
animal.cry() // error

es7中類的用法

  1. 實例屬性
    ES6 中實例的屬性只能通過構造函數中的 this.xxx 來定義,ES7 提案中可以直接在類裡面定義:
class Animal {
  name = 'Kerry';
  constructor() {
    // ...
  }
}

let a = new Animal();
console.log(a.name); // Kerry
  1. 靜態屬性
    ES7 提案中,可以使用 static 定義一個靜態屬性:
class Animal {
  static num = 42;

  constructor() {
    // ...
  }
}

console.log(Animal.num); // 42

ts中類的用法

  1. 類修飾符

ts裡面定義屬性的時候,提供了三種修飾符

public:公有,在類裡面、子類、類外面都可以訪問
protected:保護類型,在類裡面、子類裡面可以訪問,在類外面沒法訪問
private:私有,在類裡面可以訪問,子類、類外部都沒法訪問

屬性如果不加修飾符,默認就是公有public

class Person {
  public name:string
  // protected name:string
  // private name:string
  constructor(name:string){
    this.name = name
  }
  run():string{
    return `${this.name}在運動`
  }
}

class Web extends Person {
  constructor(name:string){
    super(name) // 初始化父類的構造函數
  }
  run():string{
    return `${this.name}在運動-子類`
  }
  work(): string {
    return `${this.name}在工作`
  }
}
const w = new Web('kerry')
console.log(w.run()); // kerry在運動-子類
console.log(w.work()); // kerry在工作
console.log(w.name); // kerry
  1. 抽象類
  • 抽象類是不允許被實例化
abstract class Animal {
  public name;
  public constructor(name:string) {
    this.name = name;
  }
}
let a = new Animal('Kerry'); // error
  • 抽象類中的抽象方法必須被子類實現
abstract class Animal {
  public name;
  public constructor(name:string) {
    this.name = name;
  }
  public abstract sayHi():void;
}

class Cat extends Animal {
  public sayHi() {
    console.log(`Meow, My name is ${this.name}`);
  }
}
let cat = new Cat('Kerry');
  • 給類添加類型
class Animal {
  name: string;
  constructor(name: string) {
    this.name = name;
  }
  sayHi(): string {
    return `My name is ${this.name}`;
  }
}

let a: Animal = new Animal('Kerry');
console.log(a.sayHi()); // My name is Kerry

介面和類

類實現介面

一般來講,一個類只能繼承自另一個類,有時候不同類之間可以有一些共有的特性,這時候就可以把特性提取成介面(interfaces),
用 implements 關鍵字來實現,大大提高了面向對象的靈活性

這裡舉個例子:
門是一個類,防盜門是門的子類。如果防盜門有一個報警器的功能,我們可以簡單的給防盜門添加一個報警方法。這時候如果有另一個類,車,也有報警器的功能,就可以考慮把報警器提取出來,作為一個介面,防盜門和車都去實現它

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');
    }
}

一個類可以實現多個介面

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');
    }
}

介面繼承介面

interface Alarm {
    alert(): void;
}

interface LightableAlarm extends Alarm {
    lightOn(): void;
    lightOff(): void;
}

class Car implements LightableAlarm {
    lightOn(): void {
        console.log('Car light on');
    }
    lightOff(): void {
        console.log('Car light off');
    }
    alert(): void {
        console.log('Car light on');
    }
}

類型斷言和類型推論

類型斷言

語法:值 as 類型
使用斷言,目的是告訴ts,我比你更清楚這個參數是什麼類型,以此防止報錯

function getLength(arg: number | string): number {
    const str = arg as string
    if (str.length) {
        return str.length
    } else {
        const number = arg as number
        return number.toString().length
    }
}
console.log(getLength(123)); // 3
console.log(getLength("123")); // 3

// 需要注意的是,如果把一個類型斷言成聯合類型中不存在的類型則會報錯
function getLength(arg: number | string): number {
    return (arg as number[]).length // error
}

類型推論

ts里,在有些沒有明確指出類型的地方,類型推論會幫助提供類型。

  • 定義時不賦值,ts會自動推導成 any 類型
let a
a = 18
a = 'hello'
  • 初始化變數
// 因為賦值的時候賦的是一個字元串類型,所以ts自動推導出userName是一個string類型。
let userName = 'lin'
// userName = 123  //再修改時會報錯
  • 設置默認參數值
// 會自動推導為number類型
function printAge(num = 18) {
    console.log(num)
    return num
}
printAge(20)
// printAge("Kerry") // error
  • 函數返回值
// 定義沒有返回值的函數,會自動推導返回值為void類型
function welcome() {
    console.log('hello')
}
welcome()

聯合類型、交叉類型和類型別名

  • 聯合類型 | 是指可以取幾種類型中的任意一種,而交叉類型 & 是指把幾種類型合併起來。
  • 交叉類型和 interface 的 extends 非常類似,都是為了實現對象形狀的組合和擴展。

聯合類型

如果希望一個變數可以支援多種類型,就可以用聯合類型(union types)來定義。

例如,一個變數既支援 number 類型,又支援 string 類型

// 聯合類型大大提高了類型的可擴展性
let num: number | string | boolean

num = 8
num = 'eight'
num = true

交叉類型

如果要對對象形狀進行擴展,可以使用交叉類型 &
比如 Person 有 name 和 age 的屬性,而 Student 在 name 和 age 的基礎上還有 grade 屬性
這和類的繼承是一模一樣的,這樣 Student 就繼承了 Person 上的屬性,

interface Person {
    name: string
    age: number
}

type Student = Person & { grade: number }

類型別名

類型別名,就是給類型起個別名。類型別名用 type 關鍵字來書寫,有了類型別名,我們書寫 TS 的時候可以更加方便簡潔。
比如這個例子,getName 這個函數接收的參數可能是字元串,可能是函數

type Name = string
type NameResolver = () => string
type NameOrResolver = Name | NameResolver          // 聯合類型
function getName(n: NameOrResolver): Name {
    if (typeof n === 'string') {
        return n
    }
    else {
        return n()
    }
}
// 調用
console.log(getName('kerry')); // kerry
console.log(getName(() => 'kerry')); // kerry

type 和 interface

能用interface就用interface,實現不了再考慮用type

相同點1:都可以描述一個對象或者函數

  • 描述一個對象
type IntfPerson = {
    name: string
    age: number
}
interface IntfPerson {
    name: string
    age: number
}

const person: IntfPerson = {
    name: 'kerry',
    age: 18
}
  • 描述一個函數
type addType = (num1:number,num2:number) => number

interface addType {
    (num1:number,num2:number):number
}
const add:addType = (num1, num2) => {
    return num1 + num2
}

相同點2:都可以實現繼承

interface 使用 extends 實現繼承, type 使用交叉類型實現繼承

我們定義一個 IntfPerson 類型和 Student 類型,Student 繼承自 IntfPerson

// interface 繼承 interface
interface Person { 
    name: string 
  }
  interface Student extends Person { 
    grade: number 
  }

// type 繼承 type
type IntfPerson = { 
    name: string 
}
type Student = IntfPerson & { grade: number  }   // 用交叉類型


// interface 繼承 type
type IntfPerson = { 
    name: string 
}

interface Student extends IntfPerson { 
    grade: number 
}


// type 繼承 interface
interface IntfPerson { 
    name: string 
}

type Student = IntfPerson & { grade: number  }   // 用交叉類型

不同點1:type 可以聲明基本類型、聯合類型、交叉類型、元組,interface 不行

基本類型
type Name = string
// 聯合類型
type Name = string | number
// 元組
type Name = [string, number]
交叉類型
interface IntfPerson { 
    name: string 
}
type Student = IntfPerson & { grade: number  }

let myName:Name = 'kerry'
let myName:Name = 123
let myName:Name = ['kerry',2]
let kerry:Student = {
    name:'kerry',
    grade: 1
}

不同點2: interface可以聲明合併,type不行

interface User{
    nage: string,
    age: number,
}
interface User{
    sex: string
}

let user: User = {
    nage: '小明',
    age: 10,
    sex: '男'
}

泛型

泛型是指在定義函數、介面或類的時候,不預先指定具體類型,而是在使用的時候再指定類型。可以使得輸入輸出類型統一,且可以輸入輸出任何類型。

基本用法

定義一個print函數,傳入一個string類型參數並返回一個string類型

  • 不使用泛型:
function print(arg:string):string {
    console.log(arg)
    return arg
}

假如這時候又多了一個number類型,則需要這樣寫

function print(arg:string | number):string | number {
    console.log(arg)
    return arg
}
const result:string | number = print(123)
console.log('result',result); // 123
  • 使用泛型解決
function print<T>(arg:T):T {
    console.log(arg)
    return arg
}

// 兩種方式使用:
// 方式1:定義要使用的類型
const result = print<number>(123)

// 方式2:使用ts類型自動推斷
const result = print(123)

介面中使用泛型,定義函數類型

interface IGeneric<T> {
    (arg: T): void
}
  
function fn<T>(arg: T): void {
    console.log(arg);
}
  
let myFn: IGeneric<number> = fn;
myFn(13); //13

在類中使用泛型

定義一個棧,有入棧和出棧兩個方法,如果想入棧和出棧的元素類型統一,寫法如下:

class Stack<T> {
    private data: T[] = []
    push(item:T) {
        return this.data.push(item)
    }
    pop():T | undefined {
        return this.data.pop()
    }
}

入棧和出棧是number類型
const s1 = new Stack<number>()
s1.push(1)
console.log(s1.pop()); // 1

泛型約束

假設現在有這麼一個函數,列印傳入參數的長度

function printLength<T>(arg: T): T {
    console.log(arg.length) // 因為不確定 T 是否有 length 屬性,會報錯
    return arg
}
printLength([1,2,3])

結合interface,通過extends來實現泛型約束

interface ILength {
    length: number
}
function printLength<T extends ILength>(arg: T): T {
    console.log(arg.length)
    return arg
}
printLength([1,2,3])

裝飾器

定義

  • 裝飾器是一種特殊類型的聲明,它能夠被附加到類聲明、方法、屬性或參數上,可以修改類的行為。
  • 通俗講裝飾器就是一個方法,可以注入到類、方法、屬性參數上來擴展類、屬性、方法、參數的功能
  • 常見的裝飾器有:類裝飾器、屬性裝飾器、方法裝飾器、參數裝飾器
  • 裝飾器的寫法:普通裝飾器(不可傳參)、裝飾器工廠(可傳參)

類裝飾器:普通裝飾器

function logClass(parmas:any){
  parmas.prototype.apiUrl = "//www.baidu.com"
}

@logClass
class HttpClient {
  constructor(){
  }

  getData(){

  }
}

const http:any = new HttpClient()
console.log(http.apiUrl) // //www.baidu.com

類裝飾器:裝飾器工廠

function logClass(parmas:string){
  return function(target:any){
    console.log(parmas)
    console.log(target)
  }
}

@logClass('hello')
class HttpClient {
  constructor(){
  }

  getData(){

  }
}

console列印出來結果:

hello
index.js:134 class HttpClient {
    constructor() {
    }
    getData() {
    }
}

屬性裝飾器

接收2個參數,target當前類的原型對象,attr當前屬性名稱

function logClass(parmas:string){
  return function(target:any){
    console.log(parmas)
    console.log(target)
  }
}

function logProperty(params:string){
  return function(target:any,attr:any){
    console.log(target) // 當前類的原型對象
    console.log(attr) // url 
    // 修改屬性值
    target[attr] = params
  }
}

@logClass('hello')
class HttpClient {

  @logProperty('//api.xxx.com')
  public url: string | undefined
  constructor(){
  }

  getData(){
    console.log(this.url) // //api.xxx.com
  }
}

const http = new HttpClient()
http.getData()

方法裝飾器

方法裝飾器接收三個參數:1. target當前類原型的對象 2. 當前方法名  3. 當前方法的描述符

function get(params:string){
  return function(target:any,methodName:any,desc:any){
    console.log(target) // 當前類的原型對象
    console.log(methodName) // 方法名 
    console.log(desc) // 描述符 
    // 和類裝飾器一樣,可以擴展當前類屬性和方法
    target.apiUrl='xxxx'
    target.run=function(){
      console.log('run')
    }

    // 修改裝飾器的方法  把裝飾器方法里傳入的所有參數改為string類型
    // 1. 保存當前方法 2. 參數處理並調用oMethod方法
    const oMethod = desc.value
    desc.value = function(...args:any[]){
      args = args.map((value)=>{
        return String(value)
      })
      // 調用oMethod方法,並將參數args傳入
      // 與call不同,call接受參數列表,apply接受數組形式參數
      oMethod.apply(this,args)
    }
  }
}

class HttpClient {
  public url: string | undefined
  constructor(){
  }

  @get('//api.xxx.com')
  getData(...args:any[]){
    console.log(args); // ['123', '122']
    console.log('我是getData裡面的方法')
  }
}

const http:any = new HttpClient()
console.log(http.apiUrl);
http.run()

http.getData(123,'123')

方法參數裝飾器

參數裝飾器會在運行時當做函數被調用,可以使用參數裝飾器為類的原型增加一些元素數據,傳入3個參數:

  1. 對於靜態成員來說,是類的構造函數,對於實例成員是類的原型對象
  2. 方法的名字
  3. 參數在函數參數列表中的索引
function logParams(params:any){
  return function(target:any,methodName:any,paramsIndex:any){
    console.log(params); // uuid
    console.log(target); // {constructor: ƒ, getData: ƒ}
    console.log(methodName); // getData
    console.log(paramsIndex); // 0

    // 給類的原型增加屬性
    target.apiUrl = params
  }
}

class HttpClient {
  public url: string | undefined
  constructor(){
  }

  getData(@logParams('uuid') uuid:any){
    console.log(uuid);
  }
}

const http:any = new HttpClient()
http.getData(123456)
console.log(http.apiUrl); // uuid

裝飾器執行順序

function logClass1(parmas:string){
  return function(target:any){
    console.log('類裝飾器1')
  }
}

function logClass2(parmas:string){
  return function(target:any){
    console.log('類裝飾器2')
  }
}

function logAttr(parmas?:string){
  return function(target:any,attrName:any){
    console.log('屬性裝飾器')
  }
}

function logMethod(parmas?:string){
  return function(target:any,methodName:any,desc:any){
    console.log('方法裝飾器')
  }
}

function logParams1(parmas?:string){
  return function(target:any,methodName:any,paramsIndex:any){
    console.log('方法參數裝飾器1')
  }
}

function logParams2(parmas?:string){
  return function(target:any,methodName:any,paramsIndex:any){
    console.log('方法參數裝飾器2')
  }
}

@logClass1('hello')
@logClass2('hello')
class HttpClient {

  @logAttr()
  public apiUrl:string | undefined
  constructor(){
  }

  @logMethod()
  getData(){

  }

  setData(@logParams1() attr1:any,@logParams2() attr2:any){

  }
}

const http = new HttpClient()

執行以上程式碼,可以看到執行順序是:

屬性>方法>方法參數>類

如果有多個同樣的裝飾器,它會先執行後面的

聲明文件和內置對象

聲明文件

當使用第三方庫時,很多三方庫不是用 TS 寫的,我們需要引用它的聲明文件,才能獲得對應的程式碼補全、介面提示等功能。

  • declare

在ts中如果直接使用Vue,就會報錯

const app = new Vue({
  el: '#app',
  data: {
    message: 'Hello Vue!'
  }
})

這時,我們可以使用 declare 關鍵字來定義 Vue 的類型,這樣就不會報錯了

interface VueOption {
    el: string,
    data: any
}

declare class Vue {
    options: VueOption
    constructor(options: VueOption)
}

const app = new Vue({
  el: '#app',
  data: {
    message: 'Hello Vue!'
  }
})
  • .d.ts文件

通常我們會把聲明語句放到一個單獨的文件(xxx.d.ts)中,這就是聲明文件,以 .d.ts 為後綴。
一般來說,ts 會解析項目中所有的 *.ts 文件,當然也包含以 .d.ts 結尾的文件。

  • 使用三方庫

在使用第三方庫的時候,如果社區已經為其提供了對應的類型文件,那麼我們就不需要自己再聲明一次了,直接安裝即可

例如:

// 安裝 lodash 的類型包
npm install @types/lodash -D

內置對象

JavaScript中有很多內置對象,它們可以直接在 TypeScript 中當做定義好了的類型。
內置對象是指根據標準在全局作用域 global 上存在的對象,這裡的標準指的是 ECMAcript 和其他環境(比如DOM)的標準。

  1. ECMAScript的內置對象
    Array、Boolean、Number、String、Date、RegExp、Error
const nums: Array<number> = [1,2,3]

const date: Date = new Date()

const err: Error = new Error('Error!');

const reg: RegExp = /abc/;
  1. BOM和DOM的內置對象
    Window、Document、HTMLElement、DocumentFragment、Event、NodeList
let body: HTMLElement = document.body

let allDiv: NodeList = document.querySelectorAll('div');

document.addEventListener('click', (e: MouseEvent) => {
    e.preventDefault()
  // Do something
});

[

](//localhost:3000/)

ts實戰

我們使用react+ts實現一個todo List功能

功能包括:

  • 查看任務列表
  • 添加、刪除任務
  • 完成任務

先看效果圖

image.png

初始化一個react項目

//create-react-app.dev/

  1. 初始化一個typescript項目
yarn create react-app react-ts-demo --template typescript
  1. 運行項目
cd react-ts-demo & yarn start
  1. 瀏覽器訪問://localhost:3000/

程式碼實現

  1. 創建一個父組件todolist
export interface ITodoItem {
    taskName: string,
    status: TaskStatus
}

// React.FC表示react的一個函數組件,是React.FunctionComponent的簡寫形式
const TodoList:React.FC = ()=>{

  const [list, setList] = useState<Array<ITodoItem>>([])
  const [inputValue, setInputValue] = useState<string>("")

  const updateList = (list:ITodoItem[])=>{
    setList(list)
    saveTaskList(list)
  }

  const handleAdd = ()=>{
    if(!inputValue.trim()){
        return alert("輸入不能為空")
    }
    let [...newList] = list;
    newList.push({
        taskName: inputValue,
        status: TaskStatus.PENDING
    })
    updateList(newList)
    setInputValue("")
  }


  const handleStart = (index:number)=>{
    let [...newList] = list;
    newList = newList.map((item,indx)=>{
        if(indx === index){
            return {
                ...item,
                status: TaskStatus.IN_PROGRESS
            }
        }
        return item
    })
    updateList(newList)
  }

  const handleComplete = (index:number)=>{
    let [...newList] = list;
    newList = newList.map((item,indx)=>{
        if(indx === index){
            return {
                ...item,
                status: TaskStatus.COMPLETED
            }
        }
        return item
    })
    updateList(newList)
  }


  const handleDel = (index:number)=>{
    let [...newList] = list;
    newList.splice(index,1)
    updateList(newList)
  }

  // 初始化列表數據
  useEffect(() => {
    const list = getTaskList()
    setList(list)
  }, [])
  
  
  return (
    <div className={styles.container}>
        <h1 className={styles.title}>TODO LIST</h1>
        <div className={styles.form}>
            <input className={styles.textInput} type="text" value={inputValue} placeholder="輸入任務名" onChange={(e)=>setInputValue(e.target.value)}/>
            <button className={styles.addBtn} onClick={handleAdd}>添加任務</button>
        </div>
        <div className={styles.list}>
            {list.map((item,index)=> {
                return <TodoItem key={index} index={index} item={item} handleStart={handleStart} handleComplete={handleComplete} handleDel={handleDel}/>
            })}
        </div>
    </div>
  )
}

export default TodoList;
  1. 創建一個子組件todoItem並在父組件中引入
interface IProps {
    index: number
    item: ITodoItem
    handleStart: (index: number) => void
    handleComplete: (index: number) => void
    handleDel: (index: number) => void
}

// 對於接收的組件參數,需要定義參數類型
const TodoItem:React.FC<IProps> = ({index,item,handleStart,handleComplete,handleDel})=>{
    return (
        <div className={styles.taskItem}>
            <div className={styles.name}>{item.taskName}</div>
            <div className={styles.statusText}>
                {item.status===TaskStatus.PENDING && <span className={styles.pending}>待開始</span>}
                {item.status===TaskStatus.IN_PROGRESS && <span className={styles.inProgress}>進行中</span>}
                {item.status===TaskStatus.COMPLETED && <span className={styles.completed}>已完成</span>}
            </div>
            <div className={styles.btns}>
                {item.status===TaskStatus.PENDING && (
                    <button className={styles.btn} onClick={()=>handleStart(index)}>開始</button>
                )}
                {item.status===TaskStatus.IN_PROGRESS && (
                    <button className={styles.btn} onClick={()=>handleComplete(index)}>完成</button>
                )}
                <button className={styles.btn} onClick={()=>handleDel(index)}>刪除</button>
            </div>
        </div>
    )
}

export default TodoItem
  1. 數據保存和獲取

我們在第一次進入的時候獲取初始化數據

const getTaskList = ()=> {
    const taskList = localStorage.getItem("taskList")
    if(taskList){
        return JSON.parse(taskList)
    }
    return []
}

// 初始化列表數據
  useEffect(() => {
    const list = getTaskList()
    setList(list)
  }, [])

添加、完成以及刪除的時候保存和更新數據

const saveTaskList = (list: ITodoItem[])=>{
    localStorage.setItem('taskList',JSON.stringify(list))
}

const updateList = (list:ITodoItem[])=>{
    setList(list)
    saveTaskList(list)
  }

上面程式碼我已經上傳到github,歡迎star👏🏻

參考閱讀