用Type馴化JavaScript

好多大廠都在使用 TypeScript,總感覺這玩意不看看都不配做前端了😂,看完文檔後就梳理了一下。本文來自我的博客這個想法不一定對系列,so,這個想法不一定對😉
TypeScript 具有類型系統,且是 JavaScript 的超集。它可以編譯成普通的 JavaScript 代碼。TypeScript 支持任意瀏覽器,任意環境,任意系統並且是開源的。

作爲弱類型、動態型語言,JavaScript 就像未馴化的野馬一樣。每個人都能上去坐兩下,但是真正能夠駕馭的只能是箇中好手。

近幾年,前端經歷了快速的發展已經不再是以前隨便玩玩的小玩意了。面對越來越大型、越來越持久的項目來說,這種寬鬆的方式反而成了阻礙。

東西做大了,隨之而來的就是各種規矩

規矩是從經驗中總結,同時也是爲了朝更好的方向發展,就比如編程裏的設計原則和設計模式。「Man maketh manners」,記得王牌特工裏,主角們在教育別人的時候總喜歡說這麼一句話,「不知禮,無以立也」。

在 TypeScript 裏,「禮」就是 Type,Type 就是規矩。Typescript 通過類型註解提供編譯時的靜態類型檢查,提前發現錯誤,同時也提高了代碼的可讀性和可維護性。

TypeScript 裏的類型註解是一種輕量級的爲函數或變量添加約束的方式

在 JavaScript 裏,變量用於在特定時間存儲特定值,其值及數據類型可以在腳本的生命週期內改變。

而在 TypeScript 中,標識符(變量、函數、類、屬性的名字,或者函數參數)在其定義時就指定了類型(或類型推論出)。在編譯階段,若出現了期望之外的類型,TypeScript 將會提示拋錯(雖然有時候並不會影響程序的正常運行)。

在 TypeScript 中,通過 : 類型 的方式爲標識符添加類型註解。

let isDone: boolean = false;    // boolean;
let decLiteral: number = 6;    // number;
let name: string = "bob";    // string;
let list: number[] = [1, 2, 3];    // Array<number>;
let list: Array<number> = [1, 2, 3];    // Array<number>;
let x: [string, number];    // tuple;
enum Color {Red, Green, Blue}    // enum;
let notSure: any = 4;    // any;
function warnUser(): void {    // void;
console.log("This is my warning message");
}
let u: undefined = undefined;    // undefined;
let n: null = null;    // null;
function error(message: string): never {    // never;
throw new Error(message);
}
let obj: object = {};    // object

在 TypeScript 中,數組(Array)是合併了相同類型的對象,而元組(tuple)合併了不同類型的對象。(Array<any>,也可以合併不同類型的數據)

類型註解中的類型就是以上的那些類型麼?

TypeScript 的核心原則之一是對值所具有的結構進行類型檢查,它有時被稱做「鴨式辨型法」或「結構性子類型化」。上面的只是基礎類型,它們是填充結構的基本單位而已。在 TypeScript 裏,類型不應該還停留在 JavaScript 數據類型的層面上,還應包括基礎類型的組合結構化。

let str: 'Hello';    // 字符串字面量類型;
str = 'Hi'    // error;

let something: 'Hello' | 1;    // 聯合類型;
something = 1    // ok;

let obj: {name: string, age: number};    // 對象字面量
obj = {
    name: "夜曉宸",
    age: 18,
}

換句話說,在定義標識符的時候,用一個類型模板來描述標識符的結構和內部類型組成。即類型模板就是標識符期望的樣子。

代碼是給人看的,順便是給機器運行的

都說好的代碼就該這樣。但是在 TypeScript 裏,這兩句話可以顛倒下順序。代碼是給機器運行的,順便是給人看的。
在談到 TypeScript 的好處時,有一條很重要,增強了編譯器和 IDE 的功能,包括代碼補全、接口提示、跳轉到定義、重構等。

而這些也得益於標識符的類型的精確劃分或表述,所以想寫好 Typescript 代碼,就應該精確描述標識符的類型,而不是隨處安放的 any

表述複雜結構最常用的方式 ———— 接口

接口是 JavaScript 中沒有的東西,是一個非常靈活的概念,可以抽象行爲,也可以描述「對象的形狀」。
對於需要複用的結構類型,就可以使用接口的方式,而不是對象字面量內聯式註解。

interface Iperson {    // 對象
    name: string,
    age: number,
    sayHi(): void,
}
let obj: Iperson = {
    name: "夜曉宸",
    age: 18,
    sayHi: ()=> {}
}

/* ——————人工分割線—————— */

interface Iperson {    // 函數類型
    (name: string, age: number): string
}
let person: Iperson = (name, age) => {
    return `${name},${age}`
}
person('夜曉宸', 18);

/* ——————人工分割線—————— */

interface Iperson {    // 構造函數
    new (name: string, age: number)
}
let person: Iperson = class Person {
    name: string;
    age: number;
    constructor(name, age) {
        this.name = name;
        this.age = age;
    }
}
new person('夜曉宸', 18);

/* ——————人工分割線—————— */

interface Iperson {    // 類實現接口
    name: string,
    age: number,
}
class Person implements Iperson{
    name = '夜曉宸'
    age = 18
}
new Person()

/* ——————人工分割線—————— */

interface Iperson {    // 混合類型
    (name, age): string,
    age: number,
}

function Person(): Iperson {
    let me = <Iperson>function (name, age): string {
        return `${name}, ${age}`
    }
    me.age = 18;
    return me;
}

let person = Person();
person('夜曉宸', 18)
person.age

以上是接口在對象、普通函數、構造函數、類上的表現。對於接口的屬性,還可以做到精確控制,如可選屬性、任意屬性、只讀屬性等。

最後,接口間可以繼承,接口還可以繼承類。當接口繼承類時,它會繼承類的成員但不包括其實現,但是若繼承了擁有私有或受保護的成員類時,這個接口只能由這個類或其子類來實現了,這個和類的訪問修飾符的特點有關係。

說完接口,就要說說類了,因爲它們有多相似的地方,比如充當對象的類型模板,繼承成員等。

類到底是什麼呢?

ES6 引入了 Class(類)這個概念,通過 class 關鍵字,可以定義類, Class 實質上是 JavaScript 現有的基於原型的繼承的語法糖. Class 可以通過extends關鍵字實現繼承。TypeScript 除了實現了所有 ES6 中的類的功能以外,還添加了一些新的用法。

class Person {
    static age: number = 18;
    constructor(public name: string, public age: number) { }
    sayHi(name: string): string{
        return `Hi,${name}`
    }
}
/* —————— 分割線 —————— */
var Person = /** @class */ (function () {
    function Person(name, age) {
        this.name = name;
        this.age = age;
    }
    Person.prototype.sayHi = function (name) {
        return "Hi," + name;
    };
    Person.age = 18;
    return Person;
}());

TypeScript 編譯後,可以看出來,類其實就是一個函數而已。

在 ES6 之前,通過構造函數的方式 new 出對象,造出的對象擁有和共享了構造函數內部綁定的屬性方法及原型上的屬性方法。TypeScript 裏的接口描述的類類型就是類的實例部分應該遵循的類型模板。作爲類的靜態部分 ———— 構造函數,函數也應該有自己的屬性特徵。

interface static_person {
    age: number,
    new (name: string, age: number);
}

interface instance_person {
    name: string,
    age: number,
    say(name: string): string
}

let person: static_person = class Person implements instance_person{
    static age: number = 18;
    constructor(public name: string, public age: number) { }
    say(name) {
        return `Hi,${name}`
    }
}
new person('夜曉宸',18)

由以上代碼可以看出,類的靜態部分和動態部分都有各自的類型模板。若是想要將類自身作爲類型模板又該如何做呢?最簡單的方法就是 typeof 類 的方式。

class Person {
  static age: number = 18;
    constructor(public name: string, public age: number) {}
    say(name) {
        return `Hi,${name}`
    }
}
class Man {
    static age: number;
    constructor(public name: string, public age: number) {}
    public sex = 'man';
    say(name){return `Hi, ${this.sex},${name}`}
}
let man: typeof Person = Man;
new man('夜曉宸', 18)

類靜態部分、類實例部分和類自身,它們都有自己需要遵循的類型模板。知道了其中的區別,也就能更好得理解類作爲接口使用、接口繼承類等用法了。

class Person {
  name: string;
  age: number;
}
interface Man extends Person {
  sex: 'man'
}

let man: Man = {
    name: '夜曉宸',
    age: 18,
    sex: 'man'
}

除了結構上的約束,類也通過訪問修飾符對其成員做了約束,包括 public,private,protected,readonly等。

class Person {
  private name: string;
  protected age: number;
}

interface SayPerson extends Person {
  sayHi(): string
}

class Human extends Person implements SayPerson {
  sayHi() {
    return `Hi, ${this.age}`
  }
}

知道了訪問修飾符的特點,也就明白之前說過的「當接口繼承類時,它會繼承類的成員但不包括其實現,但是若繼承了擁有私有或受保護的成員類時,這個接口只能由這個類或其子類來實現了」。

如果一個標識符的類型不確定,該如何?

對於一個內部邏輯相差不大,入參類型不同的函數來說,沒必要因爲參數類型不同而重複大部分代碼,這時就需要一個類型變量來代替。

/* 範型函數 */
class Person {
    className = 'person'
}
class Human {
    classname = 'human'
}
function create<T>(Class: new () => T) : T{
    return new Class();
}
create(Person).className

/* 範型接口 */
interface Creat<T>{
    (Class: new () => T):T
}
class Person {
    className = 'person'
}
class Human {
    classname = 'human'
}
function create<T>(Class: new () => T) : T{
    return new Class();
}
let person: Creat<Person> = create;

person(Person)    // OK
person(Human)    // Error

注意了,類型變量表示的是類型,而不是值。類型變量裏塞的可能是任意一個類型,但根據場景,我們最好能夠更加精確的描述標識符的類型。應了上面的一句話,「想寫好 Typescript 代碼,就應該精確描述標識符的類型,而不是隨處安放的 any」。所以對於泛型,我們也可以做些約束,即,泛型約束。

class Person {
  name: string;
  age: number;
}
interface Man extends Person {
  sex: 'man'
}
function getProperty<T, K extends keyof T>(obj: T, key: K): any {
  return obj[key]
}
let man: Man = {
    name: '夜曉宸',
    age: 18,
    sex: 'man'
}
getProperty(man, 'sex')

用類型變量來註釋標識符的類型有時會覺得還是不夠精確。

知道標識符的可能類型,然後組合起來
class Man {
    name: string;
    age: number;
    study():string {return ''}
}
class Women {
    name: string;
    age: number;
    sing():string{return ''}
    }
function instance(Class: Man | Women) {
    if ((<Man>Class).study) {
        return (<Man>Class).study()
    } else {
        return (<Women>Class).sing()
    }
}
let man:Man = {
    name: '夜曉宸',
    age: 18,
    study() {
        return '我愛學習';
    }
}
let women: Women = {
    name: 'godness',
    age: 17,
    sing() {
        return '我愛唱歌'
    }
}
instance(man)    // 我愛學習
instance(women)    // 我愛唱歌

有交叉類型、聯合類型等,而類型命名則是更靈活的類型組織方式。

// 官網🌰
type Name = string;
type NameResolver = () => string;
type NameOrResolver = Name | NameResolver;
function getName(n: NameOrResolver): Name {
    if (typeof n === 'string') {
        return n;
    }
    else {
        return n();
    }
}

類型多了之後,有時候需要對某一類型做特別處理,於是有類型斷言 (<類型>) 和類型守衛(typeof, instanceof, in等)。

還可以通過條件判斷來選擇哪種類型。

// 官網🌰
declare function f<T extends boolean>(x: T): T extends true ? string : number;
// Type is 'string | number
let x = f(Math.random() < 0.5)

當然了,以上代碼好多的標識符是沒有必要添加類型註解的。

類型推斷,即,類型是在哪裏如何被推斷的

類型註解也不是越多越好,即使有些地方你不添加類型註解,TypeScript 也會通過上下文歸類等方式找到最佳通用類型。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章