數棧技術分享前端篇:TS,看你哪裏逃~

數棧是—站式大數據開發平臺,咱們在github和gitee上有一個有趣的開源項目:FlinkX,FlinkX是一個基於Flink的批流統一的數據同步工具,既能夠採集靜態的數據,也能夠採集實時變化的數據,是全域、異構、批流一體的數據同步引擎。你們喜歡的話請給咱們點個star!star!star!

github開源項目:https://github.com/DTStack/flinkx

gitee開源項目:https://gitee.com/dtstack_dev_0/flinkx

有興趣的話,歡迎你們加入咱們的交流社羣:30537511(釘釘羣)

javascript

 

寫在前面

本文難度偏中下,涉及到的點大多爲如何在項目中合理應用TS,小部分會涉及一些原理,受衆面較廣,有無TS基礎都可放心食用

閱讀完本文,您可能會收穫到:

一、若您還不熟悉 TS,那本文可幫助您完成 TS 應用部分的學習,伴隨衆多 Demo 例來引導業務應用。

二、若您比較熟悉 TS,那本文可看成複習文,帶您回顧知識,但願能在某些點引起您新發現和思考。

三、針對於 class 組件的 IState 和 IProps,類比 Hook 組件的部分寫法和思考。

TIPS:超好用的在線 TS 編輯器(諸多配置項可手動配置)

傳送門:https://www.typescriptlang.org/html

什麼是 TS


不扯晦澀的概念,通俗來講 TypeScript 就是 JavaScript 的超集,它具備可選的類型,並能夠編譯爲純 JavaScript 運行。(筆者一直就把 TypeScript 看做 JavaScript 的 Lint)

那麼問題來了,爲何 TS 必定要設計成靜態的?或者換句話說,咱們爲何須要向 JavaScript 添加類型規範呢 ?

經典自問自答環節——由於它能夠解決一些 JS 還沒有解決的痛點:一、JS 是動態類型的語言,這也意味着在實例化以前咱們都不知道變量的類型,可是使用 TS 能夠在運行前就避免經典低級錯誤。

例:Uncaught TypeError:'xxx' is not a function⚠️ 典中典級別的錯誤 :

JS 就是這樣,只有在運行時發生了錯誤才告訴我有錯,可是當 TS 介入後:

好傢伙!直接把問題在編輯器階段拋出,nice!

二、懶人狂歡! 規範方便,又不容易出錯,對於 VS Code,它能作的最多隻是標示出有沒有這個屬性,但並不能精確的代表這個屬性是什麼類型,但 TS 能夠經過類型推導/反推導(說白話:若是您未明確編寫類型,則將使用類型推斷來推斷您正在使用的類型),從而完美優化了代碼補全這一項:

1)第一個 Q&A——思考 :提問:那麼咱們還能想到在業務開發中 TS 解決了哪些 JS 的痛點呢?回答,總結,補充:

    對函數參數的類型限制;
    對數組和對象的類型限制,避免定義出錯 例如數據解構複雜或較多時,可能會出現數組定義錯誤 a = { }, if (a.length){ // xxxxx }
    let functionA = 'jiawen' // 實際上 let functionA: string = 'jiawen'

三、使咱們的應用代碼更易閱讀和維護,若是定義完善,能夠經過類型大體明白參數的做用。相信經過上述簡單的bug-demo,各位已對TS有了一個初步的從新認識 接下來的章節便正式介紹咱們在業務開發過程當中如何用好TS。java

怎麼用 TS

 

在業務中如何用TS/如何用好TS?這個問題其實和 " 在業務中怎麼用好一個API " 是同樣的。首先要知道這個東西在幹嗎,參數是什麼,規則是什麼,可以接受有哪些擴展......等等。簡而言之,擼它!哪些擴展......等等。 簡而言之,擼它!

一、TS 經常使用類型概括

經過對業務中常見的 TS 錯誤作出的一個綜合性總結概括,但願 Demos 會對您有收穫
1)元語(primitives)之 string number boolean

筆者把基本類型拆開的緣由是: 無論是中文仍是英文文檔,primitives/元語/元組 這幾個名詞都頻繁出鏡,筆者理解的白話:但願在類型約束定義時,使用的是字面量而不是內置對象類型,官方文檔:

let a: string = 'jiawen';let flag: boolean = false;let num: number = 150interface IState: {  flag: boolean;  name: string;  num: number;}

2)元組

    // 元組類型表示已知元素數量和類型的數組,各元素的類型沒必要相同,可是對應位置的類型須要相同。
     
     
    let x: [string, number];
     
    x = ['jiawen', 18];   // ok
     
    x = [18, 'jiawen'];    // Erro
     
    console.log(x[0]);    // jiawen

3)undefined null

    let special: string = undefined
     
    // 值得一提的是 undefined/null 是全部基本類型的子類,
     
    // 因此它們能夠任意賦值給其餘已定義的類型,這也是爲何上述代碼不報錯的緣由

4)object 和 { }

    // object 表示的是常規的 Javascript對象類型,非基礎數據類型
     
    const offDuty = (value: object) => {
     
      console.log("value is ",  value);
     
    }
     
     
    offDuty({ prop: 0}) // ok
     
    offDuty(null) offDuty(undefined) // Error
     
    offDuty(18) offDuty('offDuty') offDuty(false) // Error
     
     
     
    //  {} 表示的是 非null / 非undefined 的任意類型
     
    const offDuty = (value: {}) => {
     
      console.log("value is ", value);
     
    }
     
     
    offDuty({ prop: 0}) // ok
     
    offDuty(null) offDuty(undefined) // Error
     
    offDuty(18) offDuty('offDuty') offDuty(false) // ok
     
    offDuty({ toString(){ return 333 } }) // ok
     
     
    //  {} 和Object幾乎一致,區別是Object會對Object內置的 toString/hasOwnPreperty 進行校驗
     
    const offDuty = (value: Object) => {
     
      console.log("value is ",  value);
     
    }
     
     
    offDuty({ prop: 0}) // ok
     
    offDuty(null) offDuty(undefined) // Error
     
    offDuty(18) offDuty('offDuty') offDuty(false) // ok
     
    offDuty({ toString(){ return 333 } }) // Error
     
     
    若是須要一個對象類型,但對屬性沒有要求,建議使用 object
     
    {} 和 Object 表示的範圍太大,建議儘可能不要使用

5)object of params

    // 咱們一般在業務中可多采用點狀對象函數(規定參數對象類型)
     
     
    const offDuty = (value: { x: number; y: string }) => {
     
      console.log("x is ", value.x);
     
      console.log("y is ", value.y);
     
    }
     
     
    // 業務中必定會涉及到"可選屬性";先簡單介紹下方便快捷的「可選屬性」
     
     
    const offDuty = (value: { x: number; y?: string }) => {
     
      console.log("必選屬性x ", value.x);
     
      console.log("可選屬性y ", value.y);
     
      console.log("可選屬性y的方法 ", value.y.toLocaleLowerCase());
     
    }
     
    offDuty({ x: 123, y: 'jiawen' })
     
    offDuty({ x: 123 })
     
     
    // 提問:上述代碼有問題嗎?
     
     
    答案:
     
     
    // offDuty({ x: 123 }) 會致使結果報錯value.y.toLocaleLowerCase()
     
    // Cannot read property 'toLocaleLowerCase' of undefined
     
     
    方案1: 手動類型檢查
     
    const offDuty = (value: { x: number; y?: string }) => {
     
      if (value.y !== undefined) {
     
          console.log("可能不存在的 ", value.y.toUpperCase());
     
      }
     
    }
     
    方案2:使用可選屬性 (推薦)
     
    const offDuty = (value: { x: number; y?: string }) => {
     
      console.log("可能不存在的 ", value.y?.toLocaleLowerCase());
     
    }

6)unknown 與 any

    // unknown 能夠表示任意類型,但它同時也告訴TS, 開發者對類型也是沒法肯定,作任何操做時須要慎重
     
     
    let Jiaven: unknown
     
     
    Jiaven.toFixed(1) // Error
     
     
    if (typeof Jiaven=== 'number') {
     
      Jiaven.toFixed(1) // OK
     
    }
     
     
    當咱們使用any類型的時候,any會逃離類型檢查,而且any類型的變量能夠執行任意操做,編譯時不會報錯
     
     
    anyscript === javascript
     
     
    注意:any 會增長了運行時出錯的風險,不到萬不得已不要使用;
     
     
    若是遇到想要表示【不知道什麼類型】的場景,推薦優先考慮 unknown
     

7)union 聯合類型

    union也叫聯合類型,由兩個或多個其餘類型組成,表示可能爲任何一個的值,類型之間用 ' | '隔開
     
     
    type dayOff = string | number | boolean
     
     
    聯合類型的隱式推導可能會致使錯誤,遇到相關問題請參考語雀 code and tips —— 《TS的隱式推導》
     
     
    .值得注意的是,若是訪問不共有的屬性的時候,會報錯,訪問共有屬性時不會.上個最直觀的demo
     
     
    function dayOff (value: string | number): number {
     
        return value.length;
     
    }
     
    // number並不具有length,會報錯,解決方法:typeof value === 'string'
     
     
    function dayOff (value: string | number): number {
     
        return value.toString();
     
    }
     
    // number和string都具有toString(),不會報錯

8)never

    // never是其它類型(包括 null 和 undefined)的子類型,表明從不會出現的值。
     
     
    // 那never在實際開發中到底有什麼做用?這裏筆者原汁原味照搬尤雨溪的經典解釋來作第一個例子
     
     
    第一個例子,當你有一個 union type:
     
     
    interface Foo {
     
      type: 'foo'
     
    }
     
     
    interface Bar {
     
      type: 'bar'
     
    }
     
     
    type All = Foo | Bar
     
     
    在 switch 當中判斷 type,TS是能夠收窄類型的 (discriminated union):
     
     
    function handleValue(val: All) {
     
      switch (val.type) {
     
        case 'foo':
     
          // 這裏 val 被收窄爲 Foo
     
          break
     
        case 'bar':
     
          // val 在這裏是 Bar
     
          break
     
        default:
     
          // val 在這裏是 never
     
          const exhaustiveCheck: never = val
     
          break
     
      }
     
    }
     
     
    注意在 default 裏面咱們把被收窄爲 never 的 val 賦值給一個顯式聲明爲 never 的變量。
     
     
     
    若是一切邏輯正確,那麼這裏應該可以編譯經過。可是假如後來有一天你的同事改了 All 的類型:
     
     
        type All = Foo | Bar | Baz
     
     
    然而他忘記了在 handleValue 裏面加上針對 Baz 的處理邏輯,
     
    這個時候在 default branch 裏面 val 會被收窄爲 Baz,致使沒法賦值給 never,產生一個編譯錯誤。
     
    因此經過這個辦法,你能夠確保 handleValue 老是窮盡 (exhaust) 了全部 All 的可能類型

    第二個用法  返回值爲 never 的函數能夠是拋出異常的狀況
     
    function error(message: string): never {
     
        throw new Error(message);
     
    }
     
     
    第三個用法 返回值爲 never 的函數能夠是沒法被執行到的終止點的狀況
     
    function loop(): never {
     
        while (true) {}
     
    }

9)Void

    interface IProps {
     
      onOK: () => void
     
    }
     
    void 和 undefined 功能高度相似,但void表示對函數的返回值並不在乎或該方法並沒有返回值

10)enum

    筆者認爲ts中的enum是一個頗有趣的枚舉類型,它的底層就是number的實現
     
     
    1.普通枚舉
     
    enum Color {
     
      Red,
     
      Green,
     
      Blue
     
    };
     
    let c: Color = Color.Blue;
     
    console.log(c); // 2
     
     
    2.字符串枚舉
     
    enum Color {
     
      Red = 'red',
     
      Green = 'not red',
     
    };
     
     
    3.異構枚舉 / 有時也叫混合枚舉
     
    enum Color {
     
      Red = 'red',
     
      Num = 2,
     
    };

    <第一個坑>
     
     
    enum Color {
     
      A,         // 0
     
      B,         // 1
     
      C = 20,    // 20
     
      D,         // 21
     
      E = 100,   // 100
     
      F,         // 101
     
    }
     
     
    若初始化有部分賦值,那麼後續成員的值爲上一個成員的值加1

    <第二個坑> 這個坑是第一個坑的延展,稍不仔細就會上當!
     
     
    const getValue = () => {
     
      return 23
     
    }
     
     
    enum List {
     
      A = getValue(),
     
      B = 24,  // 此處必需要初始化值,否則編譯不經過
     
      C
     
    }
     
    console.log(List.A) // 23
    console.log(List.B) // 24
    console.log(List.C) // 25
    若是某個屬性的值是計算出來的,那麼它後面一位的成員必需要初始化值。
    不然將會 Enum member must have initializer.

11)泛型

    筆者理解的泛型很白話:先不指定具體類型,經過傳入的參數類型來獲得具體類型 咱們從下述的 filter-demo 入手,探索一下爲何必定須要泛型

泛型的基礎樣式

    function fun<T>(args: T): T {
      return args
    }

若是沒接觸過,是否是會以爲有點懵?不要緊!咱們直接從業務角度深刻。

    1.剛開始的需求:過濾數字類型的數組
     
    declare function filter(
      array: number[],
      fn: (item: unknown) => boolean
    ) : number[];
     
    2.產品改了需求:還要過濾一些字符串 string[]
     
    彳亍,那就利用函數的重載, 加一個聲明, 雖然笨了點,可是很好理解
     
    declare function filter(
      array: string[],
      fn: (item: unknown) => boolean
    ): string[];
     
    declare function filter(
      array: number[],
      fn: (item: unknown) => boolean
    ): number[];
     
    3.產品又來了! 此次還要過濾 boolean[]、object[] ..........
     
    這個時候若是仍是選擇重載,將會大大提高工做量,代碼也會變得愈來愈累贅,這個時候泛型就出場了,
    它從實現上來講更像是一種方法,經過你的傳參來定義類型,改造以下:
     
    declare function filter<T>(
      array: T[],
      fn: (item: unknown) => boolean
    ): T[];
     

當咱們把泛型理解爲一種方法實現後,那麼咱們便很天然的聯想到:方法有多個參數、默認值,泛型也能夠。

    type Foo<T, U = string> = { // 多參數、默認值
      foo: Array<T> // 能夠傳遞
      bar: U
    }
     
    type A = Foo<number> // type A = { foo: number[]; bar: string; }
    type B = Foo<number, number> // type B = { foo: number[]; bar: number; }

既然是「函數」,那也會有「限制」,下文列舉一些稍微常見的約束。

    1. extends: 限制 T 必須至少是一個 XXX 的類型
     
    type dayOff<T extends HTMLElement = HTMLElement> = {
       where: T,
       name: string
    }

    2. Readonly<T>: 構造一個全部屬性爲readonly,這意味着沒法從新分配所構造類型的屬性。
     
    interface Eat {
      food: string;
    }
     
    const todo: Readonly<Eat> = {
      food: "meat beef milk",
    };
     
    todo.food = "no food"; // Cannot assign to 'title' because it is a read-only property.

    3. Pick<T,K>: 從T中挑選出一些K屬性
     
    interface Todo {
      name: string;
      job: string;
      work: boolean;
     
     
    type TodoPreview = Pick<Todo, "name" | "work">;
     
    const todo: TodoPreview = {
      name: "jiawen",
      work: true,
    };
    todo;

    4. Omit<T, K>: 結合了 T 和 K 並忽略對象類型中 K 來構造類型。
     
    interface Todo {
      name: string;
      job: string;
      work: boolean;
    }
     
    type TodoPreview = Omit<Todo, "work">;
     
    const todo: TodoPreview = {
      name: "jiawen",
      job: 'job',
    };
     

    5.Record: 約束 定義鍵類型爲 Keys、值類型爲 Values 的對象類型。
     
    enum Num {
      A = 10001,
      B = 10002,
      C = 10003
    }
     
    const NumMap: Record<Num, string> = {
      [Num.A]: 'this is A',
      [Num.B]: 'this is B'
    }
    // 類型 "{ 10001: string; 10002: string; }" 中缺乏屬性 "10003",
    // 但類型 "Record<ErrorCodes, string>" 中須要該屬性,因此咱們還能夠經過Record來作全面性檢查
     
    keyof 關鍵字能夠用來獲取一個對象類型的全部 key 類型
    type User = {
      id: string;
      name: string;
    };
     
    type UserKeys = keyof User;  // "id" | "name"
     
    改造以下
     
    type Record<K extends keyof any, T> = {
      [P in K]: T;
    };
    此時的 T 爲 any;

    還有一些不經常使用,可是很易懂的:
     
    6. Extract<T, U>  從T,U中提取相同的類型
     
    7. Partial<T>    全部屬性可選
     
    type User = {
      id?: string,
      gender: 'male' | 'female'
    }
     
    type PartialUser =  Partial<User>  // { id?: string, gender?: 'male' | 'female'}
      
    type Partial<T> = { [U in keyof T]?: T[U] }
     
    8. Required<T>   全部屬性必須 << === >> 與Partial相反
     
    type User = {
      id?: string,
      sex: 'male' | 'female'
    }
     
    type RequiredUser = Required<User> // { readonly id: string, readonly gender: 'male' | 'female'}
     
    function showUserProfile (user: RequiredUser) {
      console.log(user.id) // 這時候就不須要再加?了
      console.log(user.sex)
    }
    type Required<T> = { [U in keyof T]-?: T[U] };   -? : 表明去掉?
     

TS的一些須知
一、TS 的 type 和 interface

1)interface(接口) 只能聲明對象類型,支持聲明合併(可擴展)。

interface User {  id: string}interface User {  name: string}const user = {} as Userconsole.log(user.id);console.log(user.name);

2)type(類型別名)不支持聲明合併 -- l類型

    type User = {
      id: string,
    }
     
    if (true) {
      type User = {
        name: string,
      }
     
      const user = {} as User;
      console.log(user.name);
      console.log(user.id) // 類型「User」上不存在屬性「id」。
    }
     

3)type 和 interface 異同點總結:

a、一般來說 type 更爲通用,右側能夠是任意類型,包括表達式運算,以及映射等;

b、凡是可用 interface 來定義的,type 也可;

c、擴展方式也不一樣,interface 能夠用 extends 關鍵字進行擴展,或用來 implements 實現某個接口;

d、均可以用來描述一個對象或者函數;

e、type 能夠聲明基本類型別名、聯合類型、元組類型,interface 不行;

f、⚠️ 但若是你是在開發一個包,模塊,容許別人進行擴展就用 interface,若是須要定義基礎數據類型或者須要類型運算,使用 type;

g、interface 能夠被屢次定義,並會被視做合併聲明,而 type 不支持;

h、導出方式不一樣,interface 支持同時聲明並默認導出,而 typetype 必須先聲明後導出;r/>
二、TS 的腳本模式和模塊模式

Typescript 存在兩種模式,區分的邏輯是,文件內容包不包含 import 或者 export 關鍵字 。

1)腳本模式(Script), 一個文件對應一個 html 的 script 標籤 。

2)模塊模式(Module),一個文件對應一個 Typescript 的模塊。

腳本模式下,全部變量定義,類型聲明都是全局的,多個文件定義同一個變量會報錯,同名 interface 會進行合併;而模塊模式下,全部變量定義,類型聲明都是模塊內有效的。

兩種模式在編寫類型聲明時也有區別,例如腳本模式下直接 declare var GlobalStore 便可爲全局對象編寫聲明。

例子:

腳本模式下直接 declare var GlobalStore 便可爲全局對象編寫聲明。

    GlobalStore.foo = "foo";
    GlobalStore.bar = "bar"; // Error
     
    declare var GlobalStore: {
      foo: string;
    };

模塊模式下,要爲全局對象編寫聲明須要 declare global

    GlobalStore.foo = "foo";
    GlobalStore.bar = "bar";
     
    declare global {
      var GlobalStore: {
        foo: string;
        bar: string;
      };
    }
     
    export {}; // export 關鍵字改變文件的模式

三、TS 的索引簽名
索引簽名能夠用來定義對象內的屬性、值的類型,例如定義一個 React 組件,容許 Props 能夠傳任意 key 爲 string,value 爲 number 的 props

    interface Props {
      [key: string]: number
    }
     
    <Component count={1} /> // OK
    <Component count={true} /> // Error
    <Component count={'1'} /> // Error

四、TS 的類型鍵入
Typescript 容許像對象取屬性值同樣使用類型

    type User = {
      userId: string
      friendList: {
        fristName: string
        lastName: string
      }[]
    }
     
    type UserIdType = User['userId'] // string
    type FriendList = User['friendList'] // { fristName: string; lastName: string; }[]
    type Friend = FriendList[number] // { fristName: string; lastName: string; }

在上面的例子中,咱們利用類型鍵入的功能從 User 類型中計算出了其餘的幾種類型。FriendList[number]這裏的 number 是關鍵字,用來取數組子項的類型。在元組中也可使用字面量數字獲得數組元素的類型。

    type group = [number, string]
    type First =  group[0] // number
    type Second = group[1] // string

五、TS 的斷言
1)類型斷言不是類型轉換,斷言成一個聯合類型中不存在的類型是不容許的。

    function getLength(value: string | number): number {
        if (value.length) {
            return value.length;
        } else {
            return value.toString().length;
        }
     
        // 這個問題在object of parmas已經說起,再也不贅述
     
        修改後:
     
        if ((<string>value).length) {
            return (<string>value).length;
        } else {
            return something.toString().length;
        }
    }
     

    斷言的兩種寫法
     
    1. <類型>值:  <string>value
     
    2. 或者 value as string
     
    特別注意!!!斷言成一個聯合類型中不存在的類型是不容許的
     
    function toBoolean(something: string | number): boolean {
        return <boolean>something;
    }

2)非空斷言符

TypeScript 還具備一種特殊的語法,用於從類型中刪除 null 和 undefined 不進行任何顯式檢查。

在任何表達式以後寫入其實是一個類型斷言,代表該值不是 null 或 undefined

    function liveDangerously(x?: number | undefined | null) {
      // 推薦寫法
      console.log(x!.toFixed());
    }git

 

如何在 Hook 組件中使用 TS

一、usestate
useState 若是初始值不是 null/undefined 的話,是具有類型推導能力的,根據傳入的初始值推斷出類型;初始值是 null/undefined 的話則須要傳遞類型定義才能進行約束。通常狀況下,仍是推薦傳入類型(經過 useState 的第一個泛型參數)。

    // 這裏ts能夠推斷 value的類型而且能對setValue函數調用進行約束
    const [value, setValue] = useState(0);
     
    interface MyObject {
      name: string;
      age?: number;
    }
     
    // 這裏須要傳遞MyObject才能約束 value, setValue
    // 因此咱們通常狀況下推薦傳入類型
    const [value, setValue] = useState<MyObject>(null);
     

2)useEffect useLayoutEffect

沒有返回值,無需類型傳遞和約束
3)useMemo useCallback

    useMemo無需傳遞類型, 根據函數的返回值就能推斷出類型。
    useCallback無需傳遞類型,根據函數的返回值就能推斷出類型。

可是注意函數的入參須要定義類型,否則將會推斷爲any!

    const value = 10;
     
    const result = useMemo(() => value * 2, [value]); // 推斷出result是number類型
     
    const multiplier = 2;
    // 推斷出 (value: number) => number
    // 注意函數入參value須要定義類型
    const multiply = useCallback((value: number) => value * multiplier, [multiplier]);

4)useRef
useRef傳非空初始值的時候能夠推斷類型,一樣也能夠經過傳入第一個泛型參數來定義類型,約束ref.current的類型。

    1. 若是傳值爲null
    const MyInput = () => {
      const inputRef = useRef<HTMLInputElement>(null); // 這裏約束inputRef是一個html元素
      return <input ref={inputRef} />
    }
     
    2. 若是不爲null
    const myNumberRef = useRef(0);  // 自動推斷出 myNumberRef.current 是number類型
    myNumberRef.current += 1;

5)useContext
useContext通常根據傳入的Context的值就能夠推斷出返回值。通常無需顯示傳遞類型。

type Theme = 'light' | 'dark';// 咱們在createContext就傳了類型了const ThemeContext = createContext<Theme>('dark');const App = () => (  <ThemeContext.Provider value="dark">    <MyComponent />  </ThemeContext.Provider>)const MyComponent = () => {    // useContext根據ThemeContext推斷出類型,這裏不須要顯示傳  const theme = useContext(ThemeContext);  return <div>The theme is {theme}</div>github

一些思考

在本文中筆者對TS的基礎應用和Hook中的TS作了一些思考,但關於關於TSC如何把TS代碼轉換爲JS代碼的內容,這個部分比較冗長,後續能夠單獨出一篇文章(2)來專門探索。關於TS泛型的底層實現,這個部分比較複雜,筆者還需沉澱,歡迎各位直接留言或在文章中補充!!!

typescript

相關文章
相關標籤/搜索