天天看點

TS 常見問題整理(60多個,持續更新ing)

前言

  • 用 React 全家桶 + TS 寫項目快一年了,大大小小的坑踩了很多,在此整理了在項目中遇到的疑惑和問題。
  • 體會:不要畏懼 TS,别看 TS 官方文檔内容很多,其實在項目中常用的都是比較基礎的東西,像泛型運用、一些進階類型這種用的很少(封裝庫、工具函數、UI元件時用的比較多)。隻要把常用的東西看熟,最多一個小時就能上手 TS。
  • 如果本文對你有所幫助,還請點個贊,謝謝啦~~

純 TS 問題

1. TS 1.5 版本的改動

  • TypeScript 1.5 之前的版本:

    module

    關鍵字既可以稱做“内部子產品”,也可以稱做“外部子產品”。這讓剛剛接觸 TypeScript 的開發者會有些困惑。
  • TypeScript 1.5 的版本: 術語名已經發生了變化,“内部子產品”的概念更接近于大部分人眼中的“命名空間”, 是以自此之後稱作“命名空間”(也就是說 module X {…} 相當于現在推薦的寫法 namespace X {…}),而 "外部子產品" 對于 JS 來講就是子產品(ES6 子產品系統将每個檔案視為一個子產品),是以自此之後簡稱為“子產品”。
  • 不推薦使用命名空間

之前

module Math {
    export function add(x, y) { ... }
}
           

複制

之後

namespace Math {
    export function add(x, y) { ... }
}
           

複制

2. null 和 undefined 是其它類型(包括 void)的子類型,可以指派給其它類型(如:數字類型),指派後的類型會變成 null 或 undefined

  • 預設情況下,編譯器會提示錯誤,這是因為 tsconfig.json 裡面有一個配置項是預設開啟的。
// tsconfig.json 

{
      /* Strict Type-Checking Options */
    "strict": true,                           /* Enable all strict type-checking options. */
    // "noImplicitAny": true,                 /* Raise error on expressions and declarations with an implied 'any' type. */
    // 對 null 類型檢查,設定為 false 就不會報錯了
    // "strictNullChecks": true,              /* Enable strict null checks. */
    // "strictFunctionTypes": true,           /* Enable strict checking of function types. */
    // "strictBindCallApply": true,           /* Enable strict 'bind', 'call', and 'apply' methods on functions. */
    // "strictPropertyInitialization": true,  /* Enable strict checking of property initialization in classes. */
    // "noImplicitThis": true,                /* Raise error on 'this' expressions with an implied 'any' type. */
    // "alwaysStrict": true,                  /* Parse in strict mode and emit "use strict" for each source file. */
}
           

複制

  • `strictNullChecks` 參數用于新的嚴格空檢查模式,在嚴格空檢查模式下,null 和 undefined 值都不屬于任何一個類型,它們隻能指派給自己這種類型或者 any
TS 常見問題整理(60多個,持續更新ing)

3. never 和 void 的差別

  • void 表示沒有任何類型(可以被指派為 null 和 undefined)。
  • never 表示一個不包含值的類型,即表示永遠不存在的值。
  • 擁有 void 傳回值類型的函數能正常運作。擁有 never 傳回值類型的函數無法正常傳回,無法終止,或會抛出異常。

4. 元祖越界問題

let aaa: [string, number] = ['aaa', 5];
// 添加時不會報錯
aaa.push(6);
// 列印整個元祖不會報錯
console.log(aaa); // ['aaa',5,6];
// 列印添加的元素時會報錯
console.log(aaa[2]); // error
           

複制

5. 枚舉成員的特點

  • 是隻讀屬性,無法修改
  • 枚舉成員值預設從 0 開始遞增,可以自定義設定初始值
enum Gender {
    BOY = 1,
    GRIL
}
console.log(Gender.BOY);// 1
console.log(Gender);// { '1': 'BOY', '2': 'GRIL', BOY: 1, GRIL: 2 }
           

複制

  • 枚舉成員值
  • 可以沒有初始值
  • 可以是一個對常量成員的引用
  • 可以是一個常量表達式
  • 也可以是一個非常量表達式
enum Char {
    // const member 常量成員:在編譯階段被計算出結果
    a,                 // 沒有初始值
    b = Char.a,// 對常量成員的引用
    c = 1 + 3, // 常量表達式

    // computed member 計算成員:表達式保留到程式的執行階段
    d = Math.random(),// 非常量表達式
    e = '123'.length,
    // 緊跟在計算成員後面的枚舉成員必須有初始值
    f = 6,
    g
}
           

複制

6. 常量枚舉與普通枚舉的差別

  • 常量枚舉會在編譯階段被删除
  • 枚舉成員隻能是常量成員
const enum Colors {
    Red,
    Yellow,
    Blue
}
// 常量枚舉會在編譯階段被删除
let myColors = [Colors.Red, Colors.Yellow, Colors.Blue];
           

複制

編譯成 JS

"use strict";
var myColors = [0 /* Red */, 1 /* Yellow */, 2 /* Blue */];
           

複制

  • 常量枚舉不能包含計算成員,如果包含了計算成員,則會在編譯階段報錯
// 報錯
const enum Color {Red, Yellow, Blue = "blue".length};
console.log(Colors.RED);
           

複制

7. 枚舉的使用場景

以下代碼存在的問題:

  • 可讀性差:很難記住數字的含義
  • 可維護性差:寫死,後續修改的話牽一發動全身
function initByRole(role) {
    if (role === 1 || role == 2) {
        console.log("1,2")
    } else if (role == 3 || role == 4) {
        console.log('3,4')
    } else if (role === 5) {
        console.log('5')
    } else {
        console.log('')
    }
}
           

複制

使用枚舉後

enum Role {
  Reporter,
  Developer,
  Maintainer,
  Owner,
  Guest
}

function init(role: number) {
  switch (role) {
    case Role.Reporter:
      console.log("Reporter:1");
      break;
    case Role.Developer:
      console.log("Developer:2");
      break;
    case Role.Maintainer:
      console.log("Maintainer:3");
      break;
    case Role.Owner:
      console.log("Owner:4");
      break;
    default:
      console.log("Guest:5");
      break;
  }
}

init(Role.Developer);
           

複制

8. 什麼是可索引類型接口

  • 一般用來限制數組和對象
// 數字索引——限制數組
// index 是随便取的名字,可以任意取名
// 隻要 index 的類型是 number,那麼值的類型必須是 string
interface StringArray {
  // key 的類型為 number ,一般都代表是數組
  // 限制 value 的類型為 string
  [index:number]:string
}
let arr:StringArray = ['aaa','bbb'];
console.log(arr);


// 字元串索引——限制對象
// 隻要 index 的類型是 string,那麼值的類型必須是 string
interface StringObject {
  // key 的類型為 string ,一般都代表是對象
  // 限制 value 的類型為 string
  [index:string]:string
}
let obj:StringObject = {name:'ccc'};
           

複制

9. 什麼是函數類型接口

  • 對方法傳入的參數和傳回值進行限制
// 注意差別

// 普通的接口
interface discount1{
  getNum : (price:number) => number
}

// 函數類型接口
interface discount2{
  // 注意:
  // “:” 前面的是函數的簽名,用來限制函數的參數
  // ":" 後面的用來限制函數的傳回值
  (price:number):number
}
let cost:discount2 = function(price:number):number{
   return price * .8;
}

// 也可以使用類型别名
type Add = (x: number, y: number) => number
let add: Add = (a: number, b: number) => a + b
           

複制

10. 什麼是類類型接口

  • 如果接口用于一個類的話,那麼接口會表示“行為的抽象”
  • 對類的限制,讓類去實作接口,類可以實作多個接口
  • 接口隻能限制類的公有成員(執行個體屬性/方法),無法限制私有成員、構造函數、靜态屬性/方法
// 接口可以在面向對象程式設計中表示為行為的抽象
interface Speakable {
    name: string;

         // ":" 前面的是函數簽名,用來限制函數的參數
    // ":" 後面的用來限制函數的傳回值
    speak(words: string): void
}

interface Speakable2 {
    age: number;
}

class Dog implements Speakable, Speakable2 {
    name!: string;
    age = 18;

    speak(words: string) {
        console.log(words);
    }
}

let dog = new Dog();
dog.speak('汪汪汪');
           

複制

11. 什麼是混合類型接口

  • 一個對象可以同時做為函數和對象使用
interface FnType {
    (getName:string):string;
}

interface MixedType extends FnType{
    name:string;
    age:number;
}
           

複制

interface Counter {
    (start: number): string;
    interval: number;
    reset(): void;
}

function getCounter(): Counter {
    let counter = <Counter>function (start: number) { };
    counter.interval = 123;
    counter.reset = function () { };
    return counter;
}

let c = getCounter();
c(10);
c.reset();
c.interval = 5.0;
           

複制

12. 什麼是函數重載

  • 在 Java 中的函數重載,指的是兩個或者兩個以上的同名函數,參數類型不同或者參數個數不同。函數重載的好處是:不需要為功能相似的函數起不同的名稱。
  • 在 TypeScript 中,表現為給同一個函數提供多個函數類型定義,适用于接收不同的參數和傳回不同結果的情況。
  • TS 實作函數重載的時候,要求定義一系列的函數聲明,在類型最寬泛的版本中實作重載(前面的是函數聲明,目的是限制參數類型和個數,最後的函數實作是重載,表示要遵循前面的函數聲明。一般在最後的函數實作時用 any 類型)
  • 函數重載在實際應用中使用的比較少,一般會用聯合類型或泛型代替
  • 函數重載的聲明隻用于類型檢查階段,在編譯後會被删除
  • TS 編譯器在處理重載的時候,會去查詢函數申明清單,從上至下直到比對成功為止,是以要把最容易比對的類型寫到最前面
function attr(val: string): string;
function attr(val: number): number;
// 前面兩行是函數申明,這一行是實作函數重載
function attr(val: any): any {
    if (typeof val === 'string') {
        return val;
    } else if (typeof val === 'number') {
        return val;
    } 
}

attr('aaa');
attr(666);
           

複制

  • 上面的寫法聲明完函數後,必須實作函數重載。也可以隻聲明函數。
// 後寫的接口中的函數聲明優先級高
interface Cloner111 {
    clone(animal: Animal): Animal;
}
interface Cloner111 {
    clone(animal: Sheep): Sheep;
}
interface Cloner111 {
    clone(animal: Dog): Dog;
    clone(animal: Cat): Cat;
}

// ==> 同名接口會合并
// 後寫的接口中的函數聲明優先級高
interface Cloner111 {
    clone(animal: Dog): Dog;
    clone(animal: Cat): Cat;
    clone(animal: Sheep): Sheep;
    clone(animal: Animal): Animal;
}


interface Cloner222 {
        // 接口内部按書寫的順序來排,先寫的優先級高
    clone(animal: Dog): Dog;
    clone(animal: Cat): Cat;
    clone(animal: Sheep): Sheep;
    clone(animal: Animal): Animal;
}
           

複制

13. 什麼是通路控制修飾符

class Father {
    str: string; // 預設就是 public
    public name: string;   // 在定義的類中、類的執行個體、子類、子類執行個體都可以通路
    protected age: number; // 隻能在定義的類和子類中通路,不允許通過執行個體(定義的類的執行個體和子類執行個體)通路
    private money: number; // 隻能在定義的類中通路,類的執行個體、子類、子類執行個體都不可以通路
    constructor(name: string, age: number, money: number) {
        this.name = name;
        this.age = age;
        this.money = money;
    }

    getName(): string {
        return this.name;
    }

    setName(name: string): void {
        this.name = name;
    }
}

const fa = new Father('aaa', 18, 1000);
console.log(fa.name);// aaa
console.log(fa.age);// error
console.log(fa.money);// error

class Child extends Father {
    constructor(name: string, age: number, money: number) {
        super(name, age, money);
    }

    desc() {
        console.log(`${this.name} ${this.age} ${this.money}`);
    }
}

let child = new Child('bbb', 18, 1000);
console.log(child.name);// bbb
console.log(child.age);// error
console.log(child.money);// error
           

複制

14. 重寫(override) vs 重載(overload)

  • 重寫是指子類重寫“繼承”自父類中的方法 。雖然 TS 和JAVA 相似,但是 TS 中的繼承本質上還是 JS 的“繼承”機制—原型鍊機制
  • 重載是指為同一個函數提供多個類型定義
class Animal {
    speak(word: string): string {
        return '動作叫:' + word;
    }
}

class Cat extends Animal {
    speak(word: string): string {
        return '貓叫:' + word;
    }
}

let cat = new Cat();
console.log(cat.speak('hello'));

/**--------------------------------------------**/

function double(val: number): number
function double(val: string): string
function double(val: any): any {
    if (typeof val == 'number') {
        return val * 2;
    }
    return val + val;
}

let r = double(1);
console.log(r);
           

複制

15. 繼承 vs 多态

  • 繼承:子類繼承父類,子類除了擁有父類的所有特性外,還有一些更具體的特性
  • 多态:由繼承而産生了相關的不同的類,對同一個方法可以有不同的響應
class Animal {
    speak(word: string): string {
        return 'Animal: ' + word;
    }
}

class Cat extends Animal {
    speak(word: string): string {
        return 'Cat:' + word;
    }
}

class Dog extends Animal {
    speak(word: string): string {
        return 'Dog:' + word;
    }
}

let cat = new Cat();
console.log(cat.speak('hello'));
let dog = new Dog();
console.log(dog.speak('hello'));
           

複制

16. 什麼是泛型

  • 泛型是指在定義函數、接口或類的時候,不預先指定具體的類型,使用時再去指定類型的一種特性。
  • 可以把泛型了解為代表類型的參數
// 我們希望傳入的值是什麼類型,傳回的值就是什麼類型
// 傳入的值可以是任意的類型,這時候就可以用到 泛型

// 如果使用 any 的話,就失去了類型檢查的意義
function createArray1(length: any, value: any): Array<any> {
    let result: any = [];
    for (let i = 0; i < length; i++) {
        result[i] = value;
    }
    return result;
}

let result = createArray1(3, 'x');
console.log(result);

// 最傻的寫法:每種類型都得定義一種函數
function createArray2(length: number, value: string): Array<string> {
    let result: Array<string> = [];
    for (let i = 0; i < length; i++) {
        result[i] = value;
    }
    return result;
}

function createArray3(length: number, value: number): Array<number> {
    let result: Array<number> = [];
    for (let i = 0; i < length; i++) {
        result[i] = value;
    }
    return result;
}

// 或者使用函數重載,寫法有點麻煩
function createArray4(length: number, value: number): Array<number>
function createArray4(length: number, value: string): Array<string>
function createArray4(length: number, value: any): Array<any> {
    let result: Array<number> = [];
    for (let i = 0; i < length; i++) {
        result[i] = value;
    }
    return result;
}

createArray4(6, '666');
           

複制

使用泛型

// 有關聯的地方都改成 <T>
function createArray<T>(length: number, value: T): Array<T> {
    let result: T[] = [];
    for (let i = 0; i < length; i++) {
        result[i] = value;
    }
    return result;
}

// 使用的時候再指定類型
let result = createArray<string>(3, 'x');

// 也可以不指定類型,TS 會自動類型推導
let result2 = createArray(3, 'x');
console.log(result);
           

複制

17. 什麼是類型謂詞

  • 類型保護函數:要自定義一個類型保護,隻需要簡單地為這個類型保護定義一個函數即可,這個函數的傳回值是一個類型謂詞
  • 類型謂詞的文法為

    parameterName is Type

    這種形式,其中

    parameterName

    必須是目前函數簽名裡的一個參數名
interface Bird {
    fly()
    layEggs()
}
interface Fish {
    swim()
    layEggs()
}

function getSmallPet():Fish | Bird{
    return ;
}
let pet = getSmallPet();

pet.layEggs();
// 當使用聯合類型時,如果不用類型斷言,預設隻會從中擷取共有的部分
(pet as Fish).swim();
pet.swim();
           

複制

TS 常見問題整理(60多個,持續更新ing)

image.png

interface Bird {
    fly()
    layEggs()
}
interface Fish {
    swim()
    layEggs()
}

function getSmallPet():Fish | Bird{
    return ;
}
let pet = getSmallPet();

// 使用類型謂詞 
function isFish(pet:Fish | Bird):pet is Fish {
    return (pet as Fish).swim !== undefined;
}

if(isFish(pet)){
    pet.swim();
}else{
    pet.fly();
}
           

複制

TS 常見問題整理(60多個,持續更新ing)

image.png

18. 可選鍊運算符的使用

  • 可選鍊運算符是一種先檢查屬性是否存在,再嘗試通路該屬性的運算符,其符号為

    ?.

  • 如果運算符左側的操作數

    ?.

    計算為 undefined 或 null,則表達式求值為 undefined 。否則,正常觸發目标屬性通路、方法或函數調用。
  • 可選鍊運算符處于 stage3 階段,使用 @babel/plugin-proposal-optional-chaining 插件可以提前使用,TS 3.7版本正式支援使用,以前的版本會報錯
a?.b;
// 相當于 a == null ? undefined : a.b;
// 如果 a 是 null/undefined,那麼傳回 undefined,否則傳回 a.b 的值.

a?.[x];
// 相當于 a == null ? undefined : a[x];
// 如果 a 是 null/undefined,那麼傳回 undefined,否則傳回 a[x] 的值

a?.b();
// 相當于a == null ? undefined : a.b();
// 如果 a 是 null/undefined,那麼傳回 undefined
// 如果 a.b 不是函數的話,會抛類型錯誤異常,否則計算 a.b() 的結果
           

複制

19. 非空斷言符的使用

  • TS 3.7版本正式支援使用
let root: any = document.getElementById('root');
root.style.color = 'red';

let root2: (HTMLElement | null) = document.getElementById('root');
// 非空斷言操作符--> 這樣寫隻是為了騙過編譯器,防止編譯的時候報錯,打包後的代碼可能還是會報錯
root2!.style.color = 'red';
           

複制

20. 空值合并運算符的使用

  • TS 3.7版本正式支援使用
  • `||` 運算符的缺點: 當左側表達式的結果是數字 0 或空字元串時,會被視為

    false

  • 空值合并運算符:隻有左側表達式結果為 `null` 或 `undefined` 時,才會傳回右側表達式的結果。通過這種方式可以明确地區分 `undefined、null` 與 `false` 的值。
const data = {
    str:'',
    // num:0,
    flag:false,
    // flag: null,
};

// data.str 為 "" 時
let str1 = data.str || '空' // '空'
// data.num 為 0 時
let num1 =  data.num || 666 // 666
// data.flag 為 false 時
let status1 =  data.flag || true  // true


// data.str 為 "" 時,可以通過。僅在 str 為 undefined 或者 null 時,不可以通過
let st2r = data.str ?? '空';  
// data.num 為 0 時,可以通過。僅在 num 為 undefined 或者 null 時,不可以通過
let num2 = data.num ?? 666; 
// data.flag 為 false 時,可以通過。僅在 flag 為 undefined 或者 null 時,不可以通過
let status2 = data.flag ?? true;

console.log('str=>', str2);
console.log('num=>', num2);
console.log('status=>', status2);
           

複制

21. typeof class 和直接用 class 作為類型有什麼差別

class Greeter {
    static message = 'hello';

    greet(){
        return Greeter.message;
    }
}

// 擷取的是執行個體的類型,該類型可以擷取執行個體對象上的屬性/方法
let greeter1:Greeter = new Greeter();
console.log(greeter1.greet());// 'hello'


// 擷取的是類的類型,該類型可以擷取類上面的靜态屬性/方法
let greeterTwo:typeof Greeter = Greeter;
greeterTwo.message = 'hey';

let greeter2:Greeter = new greeterTwo();
console.log(greeter2.greet());// 'hey'
           

複制

22. TS 中的 never 類型具體有什麼用?

23. 當使用聯合類型時,在類型未确定的情況下,預設隻會從中擷取共有的部分

  • 使用類型斷言
interface Bird {
    fly()
    layEggs()
}
interface Fish {
    swim()
    layEggs()
}

function getSmallPet():Fish | Bird{
    return ;
}

let pet = getSmallPet();
pet.layEggs();
// 當使用聯合類型時,在類型未确定的情況下,預設隻會從中擷取共有的部分
// 需要使用類型斷言
(pet as Fish).swim();
pet.swim();
           

複制

TS 常見問題整理(60多個,持續更新ing)

image.png

  • 可區分的聯合類型(借助 never )
enum KindType{
    square = 'square',
    rectangle = 'rectangle',
    circle = 'circle',
}

interface Square {
    kind: KindType.square;
    size: number;
}

interface Rectangle {
    kind: KindType.rectangle;
    width: number;
    height: number;
}

interface Circle {
    kind: KindType.circle;
    radius: number;
}

type Shape = Square | Rectangle | Circle;

function area1(s: Shape) {
    // 如果聯合類型中的多個類型,擁有共有的屬性,那麼就可以憑借這個屬性來建立不同的類型保護區塊
    // 這裡 kind 是共有的屬性
    switch (s.kind) {
        case KindType.square:
            return s.size * s.size;
        case KindType.rectangle:
            return s.height * s.width;
        default:
            return;
    }
}
// 以上代碼有隐患,如果後續新增類型時,TS 檢查以上代碼時,雖然缺失後續新增的類型,但不會報錯
console.log(area1({kind: KindType.circle, radius: 1}));


function area2(s: Shape) {
    switch (s.kind) {
        case KindType.square:
            return s.size * s.size;
        case KindType.rectangle:
            return s.height * s.width;
        case KindType.circle:
            return Math.PI * s.radius ** 2;
        default:
            // 檢查 s 是否是 never 類型
            // 如果是 never 類型,那麼上面的分支語句都被覆寫了,就永遠都不會走到目前分支
            // 如果不是 never 類型。就說明前面的分支語句有遺漏,需要補上
            return ((e: never) => {
                throw new Error(e)
            })(s)
    }
}

console.log(area2({kind: KindType.circle, radius: 1}));
           

複制

24. What's on version 3.3.3333?

25. 在全局環境中,不能給某些變量聲明類型

let name: string;

// 加了 export 後就不會報錯
// export {} 
           

複制

TS 常見問題整理(60多個,持續更新ing)

image.png

26. 不必要的命名空間:命名空間和子產品不要混在一起使用,不要在一個子產品中使用命名空間,命名空間要在一個全局的環境中使用

你可能會寫出下面這樣的代碼:将命名空間導出

  • shapes.ts

export namespace Shapes {
    export class Triangle { /* ... */ }
    export class Square { /* ... */ }
}
           

複制

  • shapeConsumer.ts

import * as shapes from "./shapes";
let t = new shapes.Shapes.Triangle();
           

複制

不應該在子產品中使用命名空間或者說将命名空間導出: 使用命名空間是為了提供邏輯分組和避免命名沖突,子產品檔案本身已經是一個邏輯分組,并且它的名字是由導入這個子產品的代碼指定,是以沒有必要為導出的對象增加額外的子產品層。

下面是改進的例子:

  • shapes.ts

export class Triangle { /* ... */ }
export class Square { /* ... */ }
           

複制

  • shapeConsumer.ts

import * as shapes from "./shapes";
let t = new shapes.Triangle();
           

複制

或者

  • shapes.ts

namespace Shapes {
    export class Triangle { /* ... */ }
    export class Square { /* ... */ }
}
           

複制

  • shapeConsumer.ts

let t = new Shapes.Triangle();
           

複制

27. 擴充全局變量的類型

interface String {
    // 這裡是擴充,不是覆寫,是以放心使用
    double(): string;
}

String.prototype.double = function () {
    return this + '+' + this;
};
console.log('hello'.double());

// 如果加了這個,就會報錯
// export {}
           

複制

interface Window {
    myname: string
}

// 注意:這裡的 window 要小寫
console.log(window);

// 如果加了這個,目前子產品就會變成局部的
// 然後定義的類型 Window 就是局部的變量,不再是一個全局變量
// 是以上面給 Window 擴充屬性/方法就失效了
export {}
           

複制

28. export = xxx 和 import xxx = require('xxx')

  • CommonJS 和 AMD 的環境裡都有一個 exports 變量,這個變量包含了一個子產品的所有導出内容。CommonJS 和 AMD 的 exports 都可以被指派為一個對象, 這種情況下其作用就類似于 es6 文法裡的預設導出,即 export default 文法了。雖然作用相似,但是 export default 文法并不能相容 CommonJS和 AMD 的 exports。
  • 如果一個子產品遵循 ES6 子產品規範,當預設導出内容時(export default xxx),ES6 子產品系統會自動給目前子產品的頂層對象加上一個 default 屬性,指向導出的内容。當一個 ES6 子產品引入該子產品時(import moduleName from 'xxx'),ES6 子產品系統預設會自動去該子產品中的頂層對象上查找 default 屬性并将值指派給 moduleName。而如果一個非 ES6 規範的子產品引入 ES6 子產品直接使用時(var moduleName = require('xxx')),就會報錯,可以通過 moduleName.default 來使用。
  • 為了支援 CommonJS 和 AMD 的 exports,TypeScript 提供了 export = 文法。export = 文法定義一個子產品的導出對象。 這裡的對象一詞指的是類,接口,命名空間,函數或枚舉。若使用 export = 導出一個子產品,則必須使用 TypeScript 的特定文法 import module = require("module") 來導入此子產品。
// exports === module.exports // 即:這兩個變量共用一個記憶體位址

// 整體導出
// module.exports = {}

// 導出多個變量
exports.c = 3;
exports.d = 4;
           

複制

  • 一個 es6 子產品預設導出,被一個 node 子產品導入使用
// 相容性寫法隻在 TS 中有效 !!!!!!
// 相容性寫法隻在 TS 中有效 !!!!!!
// 相容性寫法隻在 TS 中有效 !!!!!!

// a.es6.ts
// 這裡隻能導出一個
export = function () {
    console.log("I'm default")
}

// b.node.ts
import fn = require('./a.es6.ts');
fn();
           

複制

29. 如何在 Node 中使用 TS

  • 安裝相關聲明檔案,如:@types/node;
  • 因為 node 子產品遵循 CommonJS 規範,一些 node 子產品(如:express)的聲明檔案,用 export = xxx 導出子產品聲明。TS 進行類型推導時,會無法推斷導緻報錯。是以需要使用 import xxx from "xxx" 或者 import xxx = "xxx" 導入 node 子產品;

30. 使用 as 替代尖括号表示類型斷言

  • 在 TS 可以使用尖括号來表示類型斷言,但是在結合 JSX 的文法時将帶來解析上的困難。是以,TS 在

    .tsx

    檔案裡禁用了使用尖括号的類型斷言。
  • as

    操作符在

    .ts

    檔案和

    .tsx

    檔案裡都可用
interface Person {
    name: string;
    age: number
}

let p1 = {age: 18} as Person;
console.log(p1.name);

// 這種寫法在 .tsx 檔案中會報錯
let p2 = <Person>{age: 18};
console.log(p2.name);
           

複制

31. 如何對 JS 檔案進行類型檢查

  • 在 tsconfig.json 中可以設定

    checkJs:true

    ,對

    .js

    檔案進行類型檢查和錯誤提示。
  • 通過在

    .js

    檔案頂部添加

    // @ts-nocheck

    注釋,讓編譯器忽略目前檔案的類型檢查。
  • 相反,你可以通過不設定

    checkJs:true

    并在

    .js

    檔案頂部添加一個

    // @ts-check

    注釋,讓編譯器檢查目前檔案。
  • 也可以在 tsconfig.json 中配置 include/exclude,選擇/排除對某些檔案進行類型檢查 。
  • 你還可以使用

    // @ts-ignore

    來忽略本行的錯誤。
  • .js

    檔案裡,類型可以和在

    .ts

    檔案裡一樣被推斷出來。當類型不能被推斷時,可以通過 JSDoc 來指定類型。
/** @type {number} */
var x;

x = 0;      // OK
x = false;  // Error: boolean is not assignable to number
           

複制

  • TS 中支援的 JSDoc 注解

32. 不要使用如下類型 Number,String,Boolean、Object,應該使用類型number、string、boolean、object

/* 錯誤 */
function reverse(s: String): String;

/* OK */
function reverse(s: string): string;
           

複制

33. 如何在解構一個函數 `function fn({ x: number }) { /* … */ }` 時,即能給變量聲明類型,又能給變量設定預設值

// error
function f({ x: number }) {
    console.log(x);
}

// ok
function f({x}: { x: number } = {x: 0}) {
    console.log(x);
}
           

複制

34. Pick摘取傳回的結果是一個對象(或者說新的接口),裡面包含摘取到的屬性

interface Test {
    arr: string[]
}
// pick 摘取傳回的結果 => {arr: string[]}
let aaa: Pick<Test, 'arr'> = {arr: ['1']};
           

複制

35. 無法使用 for of 周遊 map 資料

const map = new Map([
  ['F', 'no'],
  ['T',  'yes'],
]);
for (let key of map.keys()) {
  console.log(key);
}

// 用 forEach 也可以周遊
map.forEach((value,key) => {
 console.log(key);
});
           

複制

  • 設定 target=es5 的時候,會報錯誤,并且無法執行 for 語句

TS2569: Type 'Map' is not an array type or a string type. Use compiler. option '- downlevellteration' to allow iterating of iterators.

配置 dom.iterable 和 downlevelIteration 就可以正常運作

tsconfig.json

{
    /*當目标是ES5或ES3的時候提供對for-of、擴充運算符和解構指派中對于疊代器的完整支援*/
    "downlevelIteration": true,
  "lib": [
    "dom",
    "es5",
    "es6",
    "es7",
    "dom.iterable"
  ]
}
           

複制

  • 設定 target=es6 的時候,就能正常執行。原因:

注意:如果未指定--lib,則會注入預設的庫清單。注入的預設庫是:

► For --target ES5: DOM,ES5,ScriptHost

► For --target ES6: DOM,ES6,DOM.Iterable,ScriptHost

36. 有時候我們需要複用一個類型,但是又不需要此類型内的全部屬性,是以需要剔除某些屬性

  • 這個方法在 React 中經常用到,當父元件通過 props 向下傳遞資料的時候,通常需要複用父元件的 props 類型,但是又需要剔除一些無用的類型。
interface User {
    username: string
    id: number
    token: string
    avatar: string
    role: string
}
type UserWithoutToken = Omit<User, 'token'>
           

複制

37. 為什麼在 exclude 清單裡的子產品還會被編譯器使用

有時候是被 tsconfig.json 自動加入的,如果編譯器識别出一個檔案是子產品導入目标,它就會加到編譯清單裡,不管它是否被排除了。是以,要從編譯清單中排除一個檔案,你需要在排除它的同時,還要排除所有對它進行 import 或使用了 ///指令的檔案。

38. 使用 import xxx= namespace.xxx 建立命名空間别名

// a.ts
namespace Shape {
    const pi = Math.PI;

    export function cricle(r: number) {
        return pi * r ** 2
    }
}

// b.ts
// 直接使用
// console.log(Shape.cricle(2));

// 或者通過以下方式來使用該命名空間中的變量/函數/類
// import newName = a.b.c.d 用來給常用的、層級較深的對象起一個短的名字
// 這裡的 import 的作用是建立一個别名,為任意辨別符建立别名,包括導入的子產品中的對象
// 不要與用來加載子產品的 import x from "module-name" 文法弄混了
import cricle = Shape.cricle;
console.log(cricle(2));  
           

複制

  • 注意,這裡并沒有使用

    require

    關鍵字,而是直接使用導入符号的限定名指派。 這與使用

    var

    相似,但它還适用于類型和導入的具有命名空間含義的符号。 重要的是,對于值來講,

    import

    會生成與原始符号不同的引用,是以改變别名的

    var

    值并不會影響原始變量的值。

tsconfig.json 常用配置項注釋

{
    "compilerOptions": {

        /**************基礎配置**************/
        /**************基礎配置**************/
        /**************基礎配置**************/

        /* 開啟增量編譯:TS 編譯器在第一次編譯的時候,會生成一個存儲編譯資訊的檔案,下一次編譯的時候,會根據這個檔案進行增量的編譯,以此提高 TS 的編譯速度 */
        // "incremental": true,
        /* 指定存儲增量編譯資訊的檔案位置 */
        // "tsBuildInfoFile": "./",

        /* 列印診斷資訊 */
        // "diagnostics": true,
        /* 列印輸出的檔案 */
        // "listEmittedFiles": true,
        /* 列印編譯的檔案(包括引用的聲明檔案)*/
        // "listFiles": true,

        /* 指定 ECMAScript 的目标版本: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019' or 'ESNEXT'. */
        // "target": "es5",
        /* 指定子產品代碼的生成方式: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', or 'ESNext'. */
        // "module": "commonjs",

        /* 指定要包含在編譯中的庫檔案——引用類庫——即申明檔案,如果輸出的子產品方式是 es5,就會預設引入 "dom","es5","scripthost"  */
        /* 如果在 TS 中想要使用一些 ES6 以上版本的文法,就需要引入相關的類庫 */
        // "lib": [],

        /* 允許編譯 JS 檔案 */
        // "allowJs": true,
        /* 檢查 JS 檔案*/
        // "checkJs": true,

        /* 指定 JSX 代碼生成的模式: 'preserve', 'react-native', or 'react'. */
        /* 'react' 模式下:TS 會直接把 jsx 編譯成 js */
        /* 'preserve' 模式下:TS 不會把 jsx 編譯成 js,會保留 jsx */
        // "jsx": "preserve",


        /**************聲明檔案相關配置**************/
        /**************聲明檔案相關配置**************/
        /**************聲明檔案相關配置**************/

        /* 生成相應的類型聲明檔案 —— '.d.ts' */
        // "declaration": true,
        /* 聲明檔案的輸出路徑 */
        // "declarationDir": "./d",
        /* 隻生成聲明檔案,不生成 JS */
        // "emitDeclarationOnly": true,
        /* 聲明檔案目錄,預設 node_modules/@types */
        // "typeRoots": [],
        /* 要導入的聲明檔案包,預設導入上面聲明檔案目錄下的所有聲明檔案 */
        // "types": [],


        /* 将多個互相依賴的檔案合并并且把編譯後的内容輸出到一個檔案裡
         * 可以用在産出 AMD 子產品的場景中
         * "module":"amd" 時,當一個子產品引入了另外一個子產品,編譯的時候會把這兩個子產品的編譯結果合并到一個檔案中
         */
        // "outFile": "./",
        /* 指定編譯檔案的輸出目錄 */
        // "outDir": "./out",
        /* 指定輸入檔案的根目錄,用于控制輸出目錄的結構 */
        // "rootDir": "./",

        /* 啟用項目編譯 */
        // "composite": true,

        /*  輸出的時候移除注釋 */
        // "removeComments": true,

        /* 不輸出檔案 */
        // "noEmit": true,
        /* 發生錯誤時不輸出檔案 */
        // "noEmitOnError": true,

        /* 不生成 helper 函數,以前的話設定為 true 後,需要額外安裝 ts-helpers */
        /* 類似于 babel ,會給每個檔案都生成 helper 函數,會使得最終編譯後的包的體積變大 */
        // "noEmitHelpers": true,
        /* 現在可以通過 tslib(TS 内置的庫)引入 helper 函數,!!!檔案必須是子產品 !!! */
        /* 編譯後自動引入 var tslib_1 = require("tslib") */
        // "importHelpers": true,

        /* 當目标是 ES5 或 ES3 的時候提供對 for-of、擴充運算符和解構指派中對于疊代器的完整支援 */
        // "downlevelIteration": true,

        /* 把每一個檔案轉譯成一個單獨的子產品 */
        // "isolatedModules": true,


        /**************嚴格檢查配置**************/
        /**************嚴格檢查配置**************/
        /**************嚴格檢查配置**************/

        /* 開啟所有的嚴格檢查配置 */
        "strict": true,
        /* 不允許使用隐式的 any 類型 */
        // "noImplicitAny": true,

        /* 不允許把 null、undefined 指派給其他類型變量 */
        // "strictNullChecks": true,

        /* 不允許函數參數雙向協變 */
        // "strictFunctionTypes": true,

        /* 使用 bind/call/apply 時,嚴格檢查函數參數類型 */
        // "strictBindCallApply": true,

        /* 類的執行個體屬性必須初始化 */
        // "strictPropertyInitialization": true,

        /* 不允許 this 有隐式的 any 類型,即 this 必須有明确的指向*/
        // "noImplicitThis": true,

        /* 在嚴格模式下解析并且向每個源檔案中注入 "use strict" */
        // "alwaysStrict": true,

        /**************額外的文法檢查配置,這種檢查交給 eslint 就行,沒必要配置**************/
        /**************額外的文法檢查配置,這種檢查交給 eslint 就行,沒必要配置**************/
        /**************額外的文法檢查配置,這種檢查交給 eslint 就行,沒必要配置**************/

        /* 有未使用到的本地變量時報錯 */
        // "noUnusedLocals": true,

        /* 有未使用到的函數參數時報錯 */
        // "noUnusedParameters": true,

        /* 每個分支都要有傳回值 */
        // "noImplicitReturns": true,

        /* 嚴格校驗 switch-case 文法 */
        // "noFallthroughCasesInSwitch": true,

        /**************子產品解析配置**************/
        /**************子產品解析配置**************/
        /**************子產品解析配置**************/

        /* 指定子產品的解析政策: 'node' (Node.js) or 'classic' (TypeScript pre-1.6)*/
        /* 若未指定,那麼在使用了 --module AMD | System | ES2015 時的預設值為 Classic,其它情況時則為 Node */
        // "moduleResolution": "node",

        /* 在解析非絕對路徑子產品名的時候的基準路徑 */
        // "baseUrl": "./",

        /* 基于 'baseUrl' 的路徑映射集合 */
        // "paths": {},

        /* 将多個目錄放在一個虛拟目錄下,用于運作時 */
        /* 當自己編寫的庫和開發的代碼都輸出到一個目錄下時,開發代碼和庫的位置不一樣,開發代碼引入庫的路徑就會不對 */
        // "rootDirs": [],
        // "rootDirs": ["src","out"],

        /* 允許 export = xxx 導出 ,并使用 import xxx form "module-name" 導入*/
        // "esModuleInterop": true,

        /* 當子產品沒有預設導出的時候,允許被别的子產品預設導入,這個在代碼執行的時候沒有作用,隻是在類型檢查的時候生效 */
        // "allowSyntheticDefaultImports": true,


        /* 不要 symlinks 解析的真正路徑 */
        // "preserveSymlinks": true,

        /* 允許在子產品中以全局變量的方式通路 UMD 子產品内容 */
        // "allowUmdGlobalAccess": true,


        /************** Source Map 配置**************/
        /************** Source Map 配置**************/
        /************** Source Map 配置**************/

        /* 指定 ts 檔案位置 */
        // "sourceRoot": "",

        /* 指定 map 檔案存放的位置 */
        // "mapRoot": "",

        /* 生成目标檔案的 sourceMap */
        // "sourceMap": true,

        /* 将代碼與sourcemaps生成到一個檔案中,要求同時設定了--inlineSourceMap 或--sourceMap 屬性*/
        // "inlineSources": true,

        /* 生成目标檔案的 inline sourceMap —— 源檔案和 sourcemap 檔案在同一檔案中,而不是把 map 檔案放在一個單獨的檔案裡*/
        // "inlineSourceMap": true,

        /* 生成聲明檔案的 sourceMap */
        // "declarationMap": true,

        /************** 實驗性的配置**************/
        /************** 實驗性的配置**************/
        /************** 實驗性的配置**************/

        /* 啟用裝飾器 */
        // "experimentalDecorators": true,

        // "emitDecoratorMetadata": true,         /* Enables experimental support for emitting type metadata for decorators. */


        /**************進階配置**************/
        /**************進階配置**************/
        /**************進階配置**************/

        /* 強制區分大小寫 */
        // "forceConsistentCasingInFileNames": true

}

    /* 指定需要編譯的單個檔案清單 */
    // "files": [],

    /* 指定需要編譯的檔案/目錄 */
    // "include": [
    //    // 隻寫一個目錄名等價于 "./src/**/*"
    //    "src"
    //  ]

    /* 需要排除的檔案或目錄 */
    // "exclude": []

    /* 配置檔案繼承 */
    // "extends": "./tsconfig.base.json"

}
           

複制

tsconfig.json 配置項問題

1. 三種 JSX 模式

  • 在 TS 中想要使用 JSX 必須做兩件事:
  1. 給檔案一個 .tsx 擴充名
  2. 啟用 jsx 選項
  • TS 具有三種 JSX 模式:preserve,react 和 react-native,這些模式隻在代碼生成階段起作用,類型檢查并不受影響。
  • preserve 模式下: 不會将 JSX 編譯成 JS,生成代碼中會保留 JSX,以供後續的轉換操作使用(比如:Babel)。 另外,輸出檔案會帶有 .jsx 擴充名。
  • react 模式下: 直接将 JSX 編譯成 JS,會生成 React.createElement 的形式,在使用前不需要再進行轉換操作了,輸出檔案的擴充名為 .js。
  • react-native 模式下: 相當于 preserve,它也保留了所有的 JSX,但是輸出檔案的擴充名是 .js。
模式 輸入 輸出 輸出檔案擴充名
preserve <div /> <div /> .jsx
react <div /> React.createElement("div") .js
react-native <div /> <div /> .js

2. "lib" 配置項需要注意的問題

  • 當你安裝

    TypeScript

    時,會順帶安裝

    lib.d.ts

    等聲明檔案,此檔案包含了 JavaScript 運作時以及 DOM 中存在各種常見的環境聲明。
  • 它自動包含在 TypeScript 項目的編譯上下文中
  • 它能讓你快速開始書寫經過類型檢查的 JavaScript 代碼
  • tsconfig.json

    中的 lib 選項用來指定目前項目需要注入哪些聲明庫檔案。如果沒有指定,預設注入的庫檔案清單為:
  • --target ES5

    DOM,ES5,ScriptHost

  • --target ES6

    DOM,ES6,DOM.Iterable,ScriptHost

  • 如果在 TS 中想要使用一些 ES6 以上版本或者特殊的文法,就需要引入相關的類庫。如:

    ES7

    DOM.Iterable

3. "moduleResolution" 解析政策

https://www.tslang.cn/docs/handbook/module-resolution.html

4. 指定 target 為 es6 時,tsc 就會預設使用 "classic" 子產品解析政策,這個政策對于 `import * as abc from "@babel/types"` 這種非相對路徑的導入,不能正确解析。

  • 解決方法:指定解析政策為 node => "moduleResolution": "node"。

5. "esModuleInterop" 具體作用是什麼

  • 如果一個子產品遵循 ES6 子產品規範,當預設導出内容時(export default xxx),ES6 子產品系統會自動給目前子產品的頂層對象加上一個 default 屬性,指向導出的内容。當一個 ES6 子產品引入該子產品時(import moduleName from 'xxx'),ES6 子產品系統預設會自動去該子產品中的頂層對象上查找 default 屬性并将值指派給 moduleName。而如果一個非 ES6 規範的子產品引入 ES6 子產品直接使用時(var moduleName = require('xxx')),就會報錯,需要通過 moduleName.default 來使用。
  • TypeScript 為了相容,引入了 esModuleInterop 選項,設定 esModuleInterop 為 true ,在編譯時自動給該子產品添加 default 屬性,就可以通過 import moduleName from 'xxx' 的形式導入 非 ES6 子產品,不再需要使用 import moduleName = require('xxx') 的形式。

6. "allowSyntheticDefaultImports" 具體作用是什麼

  • 允許 預設導入 沒有設定預設導出(export default xxx)的子產品,可以以 import xxx from 'xxx' 的形式來引入子產品
// 配置前
import * as React from 'react';
import * as ReactDOM from 'react-dom';

// 配置後
import React from 'react';
import ReactDOM from 'react-dom';
           

複制

7. "paths" 配置路徑映射集合時,需要注意的問題

{
   "paths": {
      // 這裡的路徑後面必須跟着 "/*"
      "@public/*": [
        // 這裡的路徑後面必須跟着 "/*"
        "public/*"
      ],
      "@src/*": [
        "src/*"
      ],
      "@assets/*":[
        "src/assets/*"
      ],
      "@components/*": [
        "src/components/*"
      ]
    }
}
           

複制

8. "allowJs" 時需要注意的問題

  • 設定 "allowJs": false :在 .ts / .tsx 檔案中引入 .js / .jsx 檔案時,就不會有相關提示
TS 常見問題整理(60多個,持續更新ing)

image.png

React + TS 項目問題

1. 使用 import 引入非 JS 子產品會報錯,而使用 require 則沒有問題

import styles from './login.less';
import logo from '@assets/images/logo.svg';

const logo2 = require('@assets/images/logo.svg');
console.log(logo2);// path
           

複制

TS 常見問題整理(60多個,持續更新ing)

image.png

解決辦法: 給這些非 JS 子產品添加申明

/**
 * style
 */
declare module '*.css'
declare module '*.less'
// declare module "*.less" {
//     const styles: { [className: string]: string };
//     export default styles
// }
declare module '*.scss'


/**
 * 圖檔
 */
declare module '*.svg'
declare module '*.png'
declare module '*.jpg'
declare module '*.jpeg'
declare module '*.gif'
declare module '*.bmp'
           

複制

2. import * as React from 'react' 和 import React from 'react' 有什麼差別

  • 第一種寫法是将所有用 export 導出的成員指派給 React ,導入後用 React.xxx 通路
  • 第二種寫法僅是将預設導出(export default)的内容指派給 React

3. 解決 import * as xxx from 'xxx' 這種奇怪的引入方式

  • 配置 tsconfig.json
{
  // 允許 預設導入 沒有設定預設導出(export default xxx)的子產品
  // 可以以 import xxx from 'xxx' 的形式來引入子產品
    "allowSyntheticDefaultImports":true
}
           

複制

// 配置前
import * as React from 'react';
import * as ReactDOM from 'react-dom';

// 配置後
import React from 'react';
import ReactDOM from 'react-dom';
           

複制

4. 對 antd 元件庫進行按需加載

  • 這裡使用的是 ts-loader 轉譯 TS 方案,更多方案請看 Webpack 轉譯 Typescript 現有方案

.babelrc

{
  "presets": [
    "@babel/preset-react",
    "@babel/preset-env"
  ],
  "plugins": [
    [
      "import",
      {
        "libraryName": "antd",
        "libraryDirectory": "es",
        "style": "css"
        /* `style: true` 會加載 less 檔案*/
      }
    ]
  ]
}
           

複制

tsconfig.json

{
  "compilerOptions": {
    "target": "es5",
    "jsx": "preserve",// 保留 jsx
     ...
}
           

複制

webpack.config.js

{
  test: /\.tsx?$/,
    use: [
      'babel-loader',
      'ts-loader'
    ]
},
           

複制

5. 聲明通過 React.createRef()建立的 ref 類型

// 源碼
// interface RefObject<T> {
//     readonly current: T | null;
// }

const ref1:React.RefObject<HTMLDivElement> = React.createRef();

const inputRef = React.createRef<Comp>();
class EditScene extends React.Component<Props> {
    inputRef:React.RefObject<Comp> 
    constructor(props) {
        super(props);
          this.inputRef = React.createRef<Comp>();
    }
}
           

複制

6. react + redux + react-redux 項目:使用 @connect 裝飾器正常,但是一旦結合 TS 後,就會報錯

https://segmentfault.com/a/1190000016047027

import {ComponentClass} from 'react'
import {
    connect as nativeConnect,
    MapDispatchToPropsParam,
    MapStateToPropsParam
} from 'react-redux'
import {withRouter as nativeWithRouter} from 'react-router-dom'

export type ComponentDecorator<P = any> = <T extends ComponentClass<P>>(WrappedComponent: T) => T

export const connect: <P, S>(
    mapState: MapStateToPropsParam<Partial<P>, P, S>,
 // mapDispatch?: MapDispatchToPropsParam<Partial<P>, P>
    mapDispatch?: any
) => ComponentDecorator = nativeConnect as any;

export const withRouter: ComponentDecorator = nativeWithRouter as any;
           

複制

7. react + redux + react-redux 項目:在使用 mapStateToProps(state) 函數時,想要給倉庫中的 state 聲明類型

  • 借助 ReturnType
// rootReducer.ts
import {combineReducers} from 'redux';
import {connectRouter} from 'connected-react-router';
import history from '../history';
import evidenceEdit from './evidence';
import common from './common';
import work from './work';
import setScene from './set-scene';

let reducers = {
    common,
    work,
    setScene,
    evidenceEdit,
    router: connectRouter(history)
};

// 使用 ReturnType 從 rootReducer 推斷狀态形狀
// export type AppState = ReturnType<typeof rootReducer>
export type AppState = {
    [key in keyof typeof reducers]: ReturnType<typeof reducers[key]>
}

const rootReducer = combineReducers(reducers);

export default rootReducer;
           

複制

// setScene 子產品
import * as types from '../types/action-types';
import {appEditAction} from '../actions/common';

export interface SetSceneState {
    loadSuccess: boolean;
    loadProgress: number;
}

let initState: SetSceneState = {
    loadSuccess: false,
    loadProgress: 0,
};
export default function (state: SetSceneState = initState, action: appEditAction) {
    switch (action.type) {

        case types.SCENE_DATA_LOADSUCCESS: {
            return {...state, loadSuccess: action.payload.success};
        }
        case types.SCENE_DATA_LOADINGPROGRESS: {
            return {...state, loadProgress: action.payload.num};
        }
        default:
            return state;
    }
}
           

複制

使用

TS 常見問題整理(60多個,持續更新ing)

image.png

8. react + redux + react-redux 項目:想要給 action creator 函數聲明類型

// 在 Mesh 元件中
import workActions from "@store/actions/work";

interface MeshProps {
    // 剛開始我是這樣寫的,每次都得在元件的 Props 裡重新聲明一下函數
    // updateSceneData?: (workId: string,data) => appEditAction;
    updateData?: typeof workActions.updateData;
}

@connect(null, {
    updateData: workActions.updateData,
})
class Mesh extends React.Component<MeshProps> {...}
           

複制

// store/actions/work.ts

import * as types from '../types/action-types';
import {appEditAction} from "@edit-store/actions/common";

export default {
    updateWorkData(workId: string, data: any): appEditAction {
        return {type: types.UPDATE_WORK_ASYNC, payload: {workId, data}}
    }
}
           

複制

9. react + redux + react-redux 項目:給 React 元件的 Props 聲明類型(較為便捷的方法)

import * as React from 'react';
import {RouteComponentProps} from 'react-router';
import {connect} from "@store/connect";
import {AppState} from "@store/reducers";
import commonActions from "@store/actions/commonActions";

// 元件可能有四個屬性來源
// 1.mapStateToProps 的傳回值
// 2.actions 對象類型
// 3.來自路由
// 4.父元件傳進來的其它屬性

// 原先的寫法:一個個拼起來,mapStateToProps 傳回的狀态還得在 Props 接口裡再聲明一遍,比較混亂、麻煩
// interface Props {
//     loadProgress?: number;
//     markVisible?: boolean;
//     setMarkVisible?: typeof commonActions.setMarkVisible;
// }

function mapStateToProps(state: AppState) {
    const {markVisible,loadProgress} = state;
    return {
        markVisible,
        loadProgress,
    };
}

// 現在的寫法:便捷
type StateProps = ReturnType<typeof mapStateToProps>;
type DispatchProps = typeof commonActions;
interface IParams {}
type RouteProps = RouteComponentProps<IParams>;
type Props = StateProps & RouteProps & DispatchProps & {};

@connect(mapStateToProps, {
    setMarkVisible: commonActions.setMarkVisible
})
export default class App extends React.PureComponent<Props, any> {
    render() {
        const {markVisible, loadProgress} = this.props;
        return (<div > {markVisible} {loadProgress} </div>);
    }
}
           

複制

10. react + redux + react-redux 項目:想要給 redux-thunk 聲明類型

redux thunk 有一個内置類型

ThunkAction

,我們可以這樣使用:

// src/thunks.ts

import { Action } from 'redux'
import { sendMessage } from './store/chat/actions'
import { AppState } from './store'
import { ThunkAction } from 'redux-thunk'

export const thunkSendMessage = (
  message: string
): ThunkAction<void, AppState, null, Action<string>> => async dispatch => {
  const asyncResp = await exampleAPI()
  dispatch(
    sendMessage({
      message,
      user: asyncResp,
      timestamp: new Date().getTime()
    })
  )
}

function exampleAPI() {
  return Promise.resolve('Async')
}
           

複制

11. 使用 webpack 的 module.hot 會警告沒有類型定義

# 下載下傳這個類型聲明檔案
$ npm install --save @types/webpack-env
           

複制

if (process.env.NODE_ENV !== 'production') {
    if (module.hot) {
        module.hot.accept('./reducers', () => store.replaceReducer(rootReducer));
    }
}
           

複制

12. tsconfig-paths-webpack-plugin 這個包會将 tsconfig.json 中的 path 配置項内容映射到 webpack 配置中去,這樣就不需要在 webpack 中的 alias 配置項裡配置路徑映射

TS 常見問題整理(60多個,持續更新ing)

image.png

13. react 函數元件聲明

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

const Hello:React.FC<Greeting> = (props) => <h1>Hello {props.name}</h1>;
// 推薦使用第二種
const Hello2 = (props:Greeting) => <h1>Hello {props.name}</h1>;
           

複制

14. 如何編寫 react + ts 版的 HOC

import React, { Component } from 'react';

import HelloClass from './HelloClass';

interface Loading {
    loading: boolean
}

// HOC 可以接收一個類元件,也可以接收一個函數元件,是以參數的類型是 React.ComponentType
// 源碼:type ComponentType<P = {}> = ComponentClass<P> | FunctionComponent<P>;
function HelloHOC<P>(WrappedComponent: React.ComponentType<P>) {
    return class extends Component<P & Loading> {
        render() {
            const { loading, ...props } = this.props;
            return loading ? <div>Loading...</div> : <WrappedComponent { ...props as P } />;
        }
    }
}

export default HelloHOC(HelloClass);
           

複制

15. 快速擷取事件處理函數的 event 參數類型

class Login extends React.Component <Props>{

    handlerLinkBtnClick = (ev) => {
          console.log(ev);
        this.props.historyGo('./register');
    };

    handlerLinkBtnMouseMove = (ev) => {
       console.log(ev);
    };

    render() {
        return (
            <div>
                <header>
                    <p >This is Login Page </p>
                    <div className={styles.linkBtn}
                         onMouseMove={this.handlerLinkBtnMouseMove} 
                         onClick={this.handlerLinkBtnClick}>
                         Go to Register Page
                   </div>
                </header>
            </div>
        );
    }
}
           

複制

按住 Ctrl ,然後滑鼠移動到事件名上就能擷取目前事件處理函數的參數類型

TS 常見問題整理(60多個,持續更新ing)

image.png

https://juejin.im/post/5e2690dce51d454d310fb4ef