TypeScript高級(jí)用法的知識(shí)點(diǎn)匯總
引言
作為一門(mén)強(qiáng)大的靜態(tài)類(lèi)型檢查工具,如今在許多中大型應(yīng)用程序以及流行的JS庫(kù)中均能看到TypeScript的身影。JS作為一門(mén)弱類(lèi)型語(yǔ)言,在我們寫(xiě)代碼的過(guò)程中稍不留神便會(huì)修改掉變量的類(lèi)型,從而導(dǎo)致一些出乎意料的運(yùn)行時(shí)錯(cuò)誤。然而TypeScript在編譯過(guò)程中便能幫我們解決這個(gè)難題,不僅在JS中引入了強(qiáng)類(lèi)型檢查,并且編譯后的JS代碼能夠運(yùn)行在任何瀏覽器環(huán)境,Node環(huán)境和任何支持ECMAScript 3(或更高版本)的JS引擎中。最近公司剛好準(zhǔn)備使用TypeScript來(lái)對(duì)現(xiàn)有系統(tǒng)進(jìn)行重構(gòu),以前使用TypeScript的機(jī)會(huì)也不多,特別是一些有用的高級(jí)用法,所以借著這次機(jī)會(huì),重新鞏固夯實(shí)一下這方面的知識(shí)點(diǎn),如果有錯(cuò)誤的地方,還請(qǐng)指出。
1、類(lèi)繼承
在ES5中,我們一般通過(guò)函數(shù)或者基于原型的繼承來(lái)封裝一些組件公共的部分方便復(fù)用,然而在TypeScript中,我們可以像類(lèi)似Java語(yǔ)言中以面向?qū)ο蟮姆绞绞褂妙?lèi)繼承來(lái)創(chuàng)建可復(fù)用的組件。我們可以通過(guò)class關(guān)鍵字來(lái)創(chuàng)建類(lèi),并基于它使用new操作符來(lái)實(shí)例化一個(gè)對(duì)象。為了將多個(gè)類(lèi)的公共部分進(jìn)行抽象,我們可以創(chuàng)建一個(gè)父類(lèi)并讓子類(lèi)通過(guò)extends關(guān)鍵字來(lái)繼承父類(lèi),從而減少一些冗余代碼的編寫(xiě)增加代碼的可復(fù)用性和可維護(hù)性。示例如下:
class Parent { readonly x: number; constructor() { this.x = 1; } print() { console.log(this.x); } } class Child extends Parent { readonly y: number; constructor() { // 注意此處必須優(yōu)先調(diào)用super()方法 super(); this.y = 2; } print() { // 通過(guò)super調(diào)用父類(lèi)原型上的方法,但是方法中的this指向的是子類(lèi)的實(shí)例 super.print(); console.log(this.y); } } const child = new Child(); console.log(child.print()) // -> 1 2
在上述示例中,Child子類(lèi)中對(duì)父類(lèi)的print方法進(jìn)行重寫(xiě),同時(shí)在內(nèi)部使用super.print()
來(lái)調(diào)用父類(lèi)的公共邏輯,從而實(shí)現(xiàn)邏輯復(fù)用。class關(guān)鍵字作為構(gòu)造函數(shù)的語(yǔ)法糖,在經(jīng)過(guò)TypeScript編譯后,最終會(huì)被轉(zhuǎn)換為兼容性好的瀏覽器可識(shí)別的ES5代碼。class在面向?qū)ο蟮木幊谭妒街蟹浅3R?jiàn),因此為了弄清楚其背后的實(shí)現(xiàn)機(jī)制,我們不妨多花點(diǎn)時(shí)間來(lái)看下經(jīng)過(guò)編譯轉(zhuǎn)換之后的代碼是什么樣子的(當(dāng)然這部分已經(jīng)比較熟悉的同學(xué)可以直接跳過(guò))。
var __extends = (this && this.__extends) || (function () { var extendStatics = function (d, b) { extendStatics = Object.setPrototypeOf || ({ __proto__: [] } instanceof Array && function (d, b) { d.__proto__ = b; }) || function (d, b) { for (var p in b) if (b.hasOwnProperty(p)) d[p] = b[p]; }; return extendStatics(d, b); } return function (d, b) { extendStatics(d, b); function __() { this.constructor = d; } d.prototype = b === null ? Object.create(b) : (__.prototype = b.prototype, new __()); }; })(); var Parent = /** @class */ (function () { function Parent() { this.x = 1; } Parent.prototype.print = function () { console.log(this.x); }; return Parent; }()); var Child = /** @class */ (function (_super) { __extends(Child, _super); function Child() { var _this = // 注意此處必須優(yōu)先調(diào)用super()方法 _super.call(this) || this; _this.y = 2; return _this; } Child.prototype.print = function () { // 通過(guò)super調(diào)用父類(lèi)原型上的方法,但是方法中的this指向的是子類(lèi)的實(shí)例 _super.prototype.print.call(this); console.log(this.y); }; return Child; }(Parent)); var child = new Child(); console.log(child.print()); // -> 1 2
以上就是轉(zhuǎn)換后的完整代碼,為了方便對(duì)比,這里將原來(lái)的注釋信息保留,仔細(xì)研究這段代碼我們會(huì)發(fā)現(xiàn)以下幾個(gè)要點(diǎn):
1) 子類(lèi)Child的構(gòu)造函數(shù)中super()方法被轉(zhuǎn)換成了var _this = _super.call(this) || this
,這里的_super指的就是父類(lèi)Parent,因此這句代碼的含義就是調(diào)用父類(lèi)構(gòu)造函數(shù)并將this綁定到子類(lèi)的實(shí)例上,這樣的話(huà)子類(lèi)實(shí)例便可擁有父類(lèi)的x屬性。因此為了實(shí)現(xiàn)屬性繼承,我們必須在子類(lèi)構(gòu)造函數(shù)中調(diào)用super()方法,如果不調(diào)用會(huì)編譯不通過(guò)。
2) 子類(lèi)Child的print方法中super.print()
方法被轉(zhuǎn)換成了_super.prototype.print.call(this)
,這句代碼的含義就是調(diào)用父類(lèi)原型上的print方法并將方法中的this指向子類(lèi)實(shí)例,由于在上一步操作中我們已經(jīng)繼承到父類(lèi)的x屬性,因此這里我們將直接打印出子類(lèi)實(shí)例的x屬性的值。
3) extends關(guān)鍵字最終被轉(zhuǎn)換為__extends(Child, _super)
方法,其中_super指的是父類(lèi)Parent,為了方便查看,這里將_extends方法單獨(dú)提出來(lái)進(jìn)行研究。
var __extends = (this && this.__extends) || (function () { var extendStatics = function (d, b) { extendStatics = Object.setPrototypeOf || ({ __proto__: [] } instanceof Array && function (d, b) { d.__proto__ = b; }) || function (d, b) { for (var p in b) if (b.hasOwnProperty(p)) d[p] = b[p]; }; return extendStatics(d, b); } return function (d, b) { // 第一部分 extendStatics(d, b); // 第二部分 function __() { this.constructor = d; } d.prototype = b === null ? Object.create(b) : (__.prototype = b.prototype, new __()); }; })();
在以上代碼中,主要可以分為兩個(gè)部分來(lái)進(jìn)行理解,第一部分為extendStatics(d, b)
方法,第二部分為該方法后面的兩行代碼。
第一部分:
在extendStatics方法內(nèi)部雖然代碼量相對(duì)較多,但是不難發(fā)現(xiàn)其實(shí)還是主要為了兼容ES5版本的執(zhí)行環(huán)境。在ES6中新增了Object.setPrototypeOf方法用于手動(dòng)設(shè)置對(duì)象的原型,但是在ES5的環(huán)境中我們一般通過(guò)一個(gè)非標(biāo)準(zhǔn)的__proto__屬性來(lái)進(jìn)行設(shè)置,Object.setPrototypeOf方法的原理其實(shí)也是通過(guò)該屬性來(lái)設(shè)置對(duì)象的原型,其實(shí)現(xiàn)方式如下:
Object.setPrototypeOf = function(obj, proto) { obj.__proto__ = proto; return obj; }
在extendStatics(d, b)
方法中,d指子類(lèi)Child,b指父類(lèi)Parent,因此該方法的作用可以解釋為:
// 將子類(lèi)Child的__proto__屬性指向父類(lèi)Parent Child.__proto__ = Parent;
可以將這行代碼理解為構(gòu)造函數(shù)的繼承,或者叫靜態(tài)屬性和靜態(tài)方法的繼承,即屬性和方法不是掛載到構(gòu)造函數(shù)的prototype原型上的,而是直接掛載到構(gòu)造函數(shù)本身,因?yàn)樵贘S中函數(shù)本身也可以作為一個(gè)對(duì)象,并可以為其賦予任何其他的屬性,示例如下:
function Foo() { this.x = 1; this.y = 2; } Foo.bar = function() { console.log(3); } Foo.baz = 4; console.log(Foo.bar()) // -> 3 console.log(Foo.baz) // -> 4
因此當(dāng)我們?cè)谧宇?lèi)Child中以Child.someProperty
訪問(wèn)屬性時(shí),如果子類(lèi)中不存在就會(huì)通過(guò)Child.__proto__
尋找父類(lèi)的同名屬性,通過(guò)這種方式來(lái)實(shí)現(xiàn)靜態(tài)屬性和靜態(tài)方法的路徑查找。
第二部分:
在第二部分中僅包含以下兩行代碼:
function __() { this.constructor = d; } d.prototype = b === null ? Object.create(b) : (__.prototype = b.prototype, new __());
其中d指子類(lèi)Child,b指父類(lèi)Parent,這里對(duì)于JS中實(shí)現(xiàn)繼承的幾種方式比較熟悉的同學(xué)可以一眼看出,這里使用了寄生組合式繼承的方式,通過(guò)借用一個(gè)中間函數(shù)__()來(lái)避免當(dāng)修改子類(lèi)的prototype上的方法時(shí)對(duì)父類(lèi)的prototype所造成的影響。我們知道,在JS中通過(guò)構(gòu)造函數(shù)實(shí)例化一個(gè)對(duì)象之后,該對(duì)象會(huì)擁有一個(gè)__proto__屬性并指向其構(gòu)造函數(shù)的prototype屬性,示例如下:
function Foo() { this.x = 1; this.y = 2; } const foo = new Foo(); foo.__proto__ === Foo.prototype; // -> true
對(duì)于本例中,如果通過(guò)子類(lèi)Child來(lái)實(shí)例化一個(gè)對(duì)象之后,會(huì)產(chǎn)生如下關(guān)聯(lián):
const child = new Child(); child.__proto__ === (Child.prototype = new __()); child.__proto__.__proto__ === __.prototype === Parent.prototype; // 上述代碼等價(jià)于下面這種方式 Child.prototype.__proto__ === Parent.prototype;
因此當(dāng)我們?cè)谧宇?lèi)Child的實(shí)例child對(duì)象中通過(guò)child.someMethod()
調(diào)用某個(gè)方法時(shí),如果在實(shí)例中不存在該方法,則會(huì)沿著__proto__繼續(xù)往上查找,最終會(huì)經(jīng)過(guò)父類(lèi)Parent的prototype原型,即通過(guò)這種方式來(lái)實(shí)現(xiàn)方法的繼承。
基于對(duì)以上兩個(gè)部分的分析,我們可以總結(jié)出以下兩點(diǎn):
// 表示構(gòu)造函數(shù)的繼承,或者叫做靜態(tài)屬性和靜態(tài)方法的繼承,總是指向父類(lèi) 1. Child.__proto__ === Parent; // 表示方法的繼承,總是指向父類(lèi)的prototype屬性 2. Child.prototype.__proto__ === Parent.prototype;
2、訪問(wèn)修飾符
TypeScript為我們提供了訪問(wèn)修飾符(Access Modifiers)來(lái)限制在class外部對(duì)內(nèi)部屬性的訪問(wèn),訪問(wèn)修飾符主要包含以下三種:
- public:公共修飾符,其修飾的屬性和方法都是公有的,可以在任何地方被訪問(wèn)到,默認(rèn)情況下所有屬性和方法都是public的。
- private:私有修飾符,其修飾的屬性和方法在class外部不可見(jiàn)。
- protected:受保護(hù)修飾符,和private比較相似,但是其修飾的屬性和方法在子類(lèi)內(nèi)部是被允許訪問(wèn)的。
我們通過(guò)一些示例來(lái)對(duì)幾種修飾符進(jìn)行對(duì)比:
class Human { public name: string; public age: number; public constructor(name: string, age: number) { this.name = name; this.age = age; } } const man = new Human('tom', 20); console.log(man.name, man.age); // -> tom 20 man.age = 21; console.log(man.age); // -> 21
在上述示例中,由于我們將訪問(wèn)修飾符設(shè)置為public,因此我們通過(guò)實(shí)例man來(lái)訪問(wèn)name和age屬性是被允許的,同時(shí)對(duì)age屬性重新賦值也是允許的。但是在某些情況下,我們希望某些屬性是對(duì)外不可見(jiàn)的,同時(shí)不允許被修改,那么我們就可以使用private修飾符:
class Human { public name: string; private age: number; // 此處修改為使用private修飾符 public constructor(name: string, age: number) { this.name = name; this.age = age; } } const man = new Human('tom', 20); console.log(man.name); // -> tom console.log(man.age); // -> Property 'age' is private and only accessible within class 'Human'.
我們將age屬性的修飾符修改為private后,在外部通過(guò)man.age對(duì)其進(jìn)行訪問(wèn),TypeScript在編譯階段就會(huì)發(fā)現(xiàn)其是一個(gè)私有屬性并最終將會(huì)報(bào)錯(cuò)。
注意:在TypeScript編譯之后的代碼中并沒(méi)有限制對(duì)私有屬性的存取操作。
編譯后的代碼如下:
var Human = /** @class */ (function () { function Human(name, age) { this.name = name; this.age = age; } return Human; }()); var man = new Human('tom', 20); console.log(man.name); // -> tom console.log(man.age); // -> 20
使用private修飾符修飾的屬性或者方法在子類(lèi)中也是不允許訪問(wèn)的,示例如下:
class Human { public name: string; private age: number; public constructor(name: string, age: number) { this.name = name; this.age = age; } } class Woman extends Human { private gender: number = 0; public constructor(name: string, age: number) { super(name, age); console.log(this.age); } } const woman = new Woman('Alice', 18); // -> Property 'age' is private and only accessible within class 'Human'.
在上述示例中由于在父類(lèi)Human中age屬性被設(shè)置為private,因此在子類(lèi)Woman中無(wú)法訪問(wèn)到age屬性,為了讓在子類(lèi)中允許訪問(wèn)age屬性,我們可以使用protected修飾符來(lái)對(duì)其進(jìn)行修飾:
class Human { public name: string; protected age: number; // 此處修改為使用protected修飾符 public constructor(name: string, age: number) { this.name = name; this.age = age; } } class Woman extends Human { private gender: number = 0; public constructor(name: string, age: number) { super(name, age); console.log(this.age); } } const woman = new Woman('Alice', 18); // -> 18
當(dāng)我們將private修飾符用于構(gòu)造函數(shù)時(shí),則表示該類(lèi)不允許被繼承或?qū)嵗?,示例如下?/p>
class Human { public name: string; public age: number; // 此處修改為使用private修飾符 private constructor(name: string, age: number) { this.name = name; this.age = age; } } class Woman extends Human { private gender: number = 0; public constructor(name: string, age: number) { super(name, age); } } const man = new Human('Alice', 18); // -> Cannot extend a class 'Human'. Class constructor is marked as private. // -> Constructor of class 'Human' is private and only accessible within the class declaration.
當(dāng)我們將protected修飾符用于構(gòu)造函數(shù)時(shí),則表示該類(lèi)只允許被繼承,示例如下:
class Human { public name: string; public age: number; // 此處修改為使用protected修飾符 protected constructor(name: string, age: number) { this.name = name; this.age = age; } } class Woman extends Human { private gender: number = 0; public constructor(name: string, age: number) { super(name, age); } } const man = new Human('Alice', 18); // -> Constructor of class 'Human' is protected and only accessible within the class declaration.
另外我們還可以直接將修飾符放到構(gòu)造函數(shù)的參數(shù)中,示例如下:
class Human { // public name: string; // private age: number; public constructor(public name: string, private age: number) { this.name = name; this.age = age; } } const man = new Human('tom', 20); console.log(man.name); // -> tom console.log(man.age); // -> Property 'age' is private and only accessible within class 'Human'.
3、接口與構(gòu)造器簽名
當(dāng)我們的項(xiàng)目中擁有很多不同的類(lèi)時(shí)并且這些類(lèi)之間可能存在某方面的共同點(diǎn),為了描述這種共同點(diǎn),我們可以將其提取到一個(gè)接口(interface)中用于集中維護(hù),并使用implements關(guān)鍵字來(lái)實(shí)現(xiàn)這個(gè)接口,示例如下:
interface IHuman { name: string; age: number; walk(): void; } class Human implements IHuman { public constructor(public name: string, public age: number) { this.name = name; this.age = age; } walk(): void { console.log('I am walking...'); } }
上述代碼在編譯階段能順利通過(guò),但是我們注意到在Human類(lèi)中包含constructor構(gòu)造函數(shù),如果我們想在接口中為該構(gòu)造函數(shù)定義一個(gè)簽名并讓Human類(lèi)來(lái)實(shí)現(xiàn)這個(gè)接口,看會(huì)發(fā)生什么:
interface HumanConstructor { new (name: string, age: number); } class Human implements HumanConstructor { public constructor(public name: string, public age: number) { this.name = name; this.age = age; } walk(): void { console.log('I am walking...'); } } // -> Class 'Human' incorrectly implements interface 'HumanConstructor'. // -> Type 'Human' provides no match for the signature 'new (name: string, age: number): any'.
然而TypeScript會(huì)編譯出錯(cuò),告訴我們錯(cuò)誤地實(shí)現(xiàn)了HumanConstructor接口,這是因?yàn)楫?dāng)一個(gè)類(lèi)實(shí)現(xiàn)一個(gè)接口時(shí),只會(huì)對(duì)實(shí)例部分進(jìn)行編譯檢查,類(lèi)的靜態(tài)部分是不會(huì)被編譯器檢查的。因此這里我們嘗試換種方式,直接操作類(lèi)的靜態(tài)部分,示例如下:
interface HumanConstructor { new (name: string, age: number); } interface IHuman { name: string; age: number; walk(): void; } class Human implements IHuman { public constructor(public name: string, public age: number) { this.name = name; this.age = age; } walk(): void { console.log('I am walking...'); } } // 定義一個(gè)工廠方法 function createHuman(constructor: HumanConstructor, name: string, age: number): IHuman { return new constructor(name, age); } const man = createHuman(Human, 'tom', 18); console.log(man.name, man.age); // -> tom 18
在上述示例中通過(guò)額外創(chuàng)建一個(gè)工廠方法createHuman并將構(gòu)造函數(shù)作為第一個(gè)參數(shù)傳入,此時(shí)當(dāng)我們調(diào)用createHuman(Human, 'tom', 18)
時(shí)編譯器便會(huì)檢查第一個(gè)參數(shù)是否符合HumanConstructor接口的構(gòu)造器簽名。
4、聲明合并
在聲明合并中最常見(jiàn)的合并類(lèi)型就是接口了,因此這里先從接口開(kāi)始介紹幾種比較常見(jiàn)的合并方式。
4.1 接口合并
示例代碼如下:
interface A { name: string; } interface A { age: number; } // 等價(jià)于 interface A { name: string; age: number; } const a: A = {name: 'tom', age: 18};
接口合并的方式比較容易理解,即聲明多個(gè)同名的接口,每個(gè)接口中包含不同的屬性聲明,最終這些來(lái)自多個(gè)接口的屬性聲明會(huì)被合并到同一個(gè)接口中。
注意:所有同名接口中的非函數(shù)成員必須唯一,如果不唯一則必須保證類(lèi)型相同,否則編譯器會(huì)報(bào)錯(cuò)。對(duì)于函數(shù)成員,后聲明的同名接口會(huì)覆蓋掉之前聲明的同名接口,即后聲明的同名接口中的函數(shù)相當(dāng)于一次重載,具有更高的優(yōu)先級(jí)。
4.2 函數(shù)合并
函數(shù)的合并可以簡(jiǎn)單理解為函數(shù)的重載,即通過(guò)同時(shí)定義多個(gè)不同類(lèi)型參數(shù)或不同類(lèi)型返回值的同名函數(shù)來(lái)實(shí)現(xiàn),示例代碼如下:
// 函數(shù)定義 function foo(x: number): number; function foo(x: string): string; // 函數(shù)具體實(shí)現(xiàn) function foo(x: number | string): number | string { if (typeof x === 'number') { return (x).toFixed(2); } return x.substring(0, x.length - 1); }
在上述示例中,我們對(duì)foo函數(shù)進(jìn)行多次定義,每次定義的函數(shù)參數(shù)類(lèi)型不同,返回值類(lèi)型不同,最后一次為函數(shù)的具體實(shí)現(xiàn),在實(shí)現(xiàn)中只有在兼容到前面的所有定義時(shí),編譯器才不會(huì)報(bào)錯(cuò)。
注意:TypeScript編譯器會(huì)優(yōu)先從最開(kāi)始的函數(shù)定義進(jìn)行匹配,因此如果多個(gè)函數(shù)定義存在包含關(guān)系,則需要將最精確的函數(shù)定義放到最前面,否則將始終不會(huì)被匹配到。
4.3 類(lèi)型別名聯(lián)合
類(lèi)型別名聯(lián)合與接口合并有所區(qū)別,類(lèi)型別名不會(huì)新建一個(gè)類(lèi)型,只是創(chuàng)建一個(gè)新的別名來(lái)對(duì)多個(gè)類(lèi)型進(jìn)行引用,同時(shí)不能像接口一樣被實(shí)現(xiàn)(implements)和繼承(extends),示例如下:
type HumanProperty = { name: string; age: number; gender: number; }; type HumanBehavior = { eat(): void; walk(): void; } type Human = HumanProperty & HumanBehavior; let woman: Human = { name: 'tom', age: 18, gender: 0, eat() { console.log('I can eat.'); }, walk() { console.log('I can walk.'); } } class HumanComponent extends Human { constructor(public name: string, public age: number, public gender: number) { this.name = name; this.age = age; this.gender = gender; } eat() { console.log('I can eat.'); } walk() { console.log('I can walk.'); } } // -> 'Human' only refers to a type, but is being used as a value here.
5、keyof 索引查詢(xún)
在TypeScript中的keyof有點(diǎn)類(lèi)似于JS中的Object.keys()
方法,但是區(qū)別在于前者遍歷的是類(lèi)型中的字符串索引,后者遍歷的是對(duì)象中的鍵名,示例如下:
interface Rectangle { x: number; y: number; width: number; height: number; } type keys = keyof Rectangle; // 等價(jià)于 type keys = "x" | "y" | "width" | "height"; // 這里使用了泛型,強(qiáng)制要求第二個(gè)參數(shù)的參數(shù)名必須包含在第一個(gè)參數(shù)的所有字符串索引中 function getRectProperty<T extends object, K extends keyof T>(rect: T, property: K): T[K] { return rect[property]; } let rect: Rectangle = { x: 50, y: 50, width: 100, height: 200 }; console.log(getRectProperty(rect, 'width')); // -> 100 console.log(getRectProperty(rect, 'notExist')); // -> Argument of type '"notExist"' is not assignable to parameter of type '"width" | "x" | "y" | "height"'.
在上述示例中我們通過(guò)使用keyof來(lái)限制函數(shù)的參數(shù)名property必須被包含在類(lèi)型Rectangle的所有字符串索引中,如果沒(méi)有被包含則編譯器會(huì)報(bào)錯(cuò),可以用來(lái)在編譯時(shí)檢測(cè)對(duì)象的屬性名是否書(shū)寫(xiě)有誤。
6、Partial 可選屬性
在某些情況下,我們希望類(lèi)型中的所有屬性都不是必需的,只有在某些條件下才存在,我們就可以使用Partial來(lái)將已聲明的類(lèi)型中的所有屬性標(biāo)識(shí)為可選的,示例如下:
// 該類(lèi)型已內(nèi)置在TypeScript中 type Partial<T> = { [P in keyof T]?: T[P] }; interface Rectangle { x: number; y: number; width: number; height: number; } type PartialRectangle = Partial<Rectangle>; // 等價(jià)于 type PartialRectangle = { x?: number; y?: number; width?: number; height?: number; } let rect: PartialRectangle = { width: 100, height: 200 };
在上述示例中由于我們使用Partial將所有屬性標(biāo)識(shí)為可選的,因此最終rect對(duì)象中雖然只包含width和height屬性,但是編譯器依舊沒(méi)有報(bào)錯(cuò),當(dāng)我們不能明確地確定對(duì)象中包含哪些屬性時(shí),我們就可以通過(guò)Partial來(lái)聲明。
7、Pick 部分選擇
在某些應(yīng)用場(chǎng)景下,我們可能需要從一個(gè)已聲明的類(lèi)型中抽取出一個(gè)子類(lèi)型,在子類(lèi)型中包含父類(lèi)型中的部分或全部屬性,這時(shí)我們可以使用Pick來(lái)實(shí)現(xiàn),示例代碼如下:
// 該類(lèi)型已內(nèi)置在TypeScript中 type Pick<T, K extends keyof T> = { [P in K]: T[P] }; interface User { id: number; name: string; age: number; gender: number; email: string; } type PickUser = Pick<User, "id" | "name" | "gender">; // 等價(jià)于 type PickUser = { id: number; name: string; gender: number; }; let user: PickUser = { id: 1, name: 'tom', gender: 1 };
在上述示例中,由于我們只關(guān)心user對(duì)象中的id,name和gender是否存在,其他屬性不做明確規(guī)定,因此我們就可以使用Pick從User接口中揀選出我們關(guān)心的屬性而忽略其他屬性的編譯檢查。
8、never 永不存在
never表示的是那些永不存在的值的類(lèi)型,比如在函數(shù)中拋出異?;蛘邿o(wú)限循環(huán),never類(lèi)型可以是任何類(lèi)型的子類(lèi)型,也可以賦值給任何類(lèi)型,但是相反卻沒(méi)有一個(gè)類(lèi)型可以作為never類(lèi)型的子類(lèi)型,示例如下:
// 函數(shù)拋出異常 function throwError(message: string): never { throw new Error(message); } // 函數(shù)自動(dòng)推斷出返回值為never類(lèi)型 function reportError(message: string) { return throwError(message); } // 無(wú)限循環(huán) function loop(): never { while(true) { console.log(1); } } // never類(lèi)型可以是任何類(lèi)型的子類(lèi)型 let n: never; let a: string = n; let b: number = n; let c: boolean = n; let d: null = n; let e: undefined = n; let f: any = n; // 任何類(lèi)型都不能賦值給never類(lèi)型 let a: string = '123'; let b: number = 0; let c: boolean = true; let d: null = null; let e: undefined = undefined; let f: any = []; let n: never = a; // -> Type 'string' is not assignable to type 'never'. let n: never = b; // -> Type 'number' is not assignable to type 'never'. let n: never = c; // -> Type 'true' is not assignable to type 'never'. let n: never = d; // -> Type 'null' is not assignable to type 'never'. let n: never = e; // -> Type 'undefined' is not assignable to type 'never'. let n: never = f; // -> Type 'any' is not assignable to type 'never'.
9、Exclude 屬性排除
與Pick相反,Pick用于揀選出我們需要關(guān)心的屬性,而Exclude用于排除掉我們不需要關(guān)心的屬性,示例如下:
// 該類(lèi)型已內(nèi)置在TypeScript中 // 這里使用了條件類(lèi)型(Conditional Type),和JS中的三目運(yùn)算符效果一致 type Exclude<T, U> = T extends U ? never : T; interface User { id: number; name: string; age: number; gender: number; email: string; } type keys = keyof User; // -> "id" | "name" | "age" | "gender" | "email" type ExcludeUser = Exclude<keys, "age" | "email">; // 等價(jià)于 type ExcludeUser = "id" | "name" | "gender";
在上述示例中我們通過(guò)在ExcludeUser中傳入我們不需要關(guān)心的age和email屬性,Exclude會(huì)幫助我們將不需要的屬性進(jìn)行剔除,留下的屬性id,name和gender即為我們需要關(guān)心的屬性。一般來(lái)說(shuō),Exclude很少單獨(dú)使用,可以與其他類(lèi)型配合實(shí)現(xiàn)更復(fù)雜更有用的功能。
10、Omit 屬性忽略
在上一個(gè)用法中,我們使用Exclude來(lái)排除掉其他不需要的屬性,但是在上述示例中的寫(xiě)法耦合度較高,當(dāng)有其他類(lèi)型也需要這樣處理時(shí),就必須再實(shí)現(xiàn)一遍相同的邏輯,不妨我們?cè)龠M(jìn)一步封裝,隱藏這些底層的處理細(xì)節(jié),只對(duì)外暴露簡(jiǎn)單的公共接口,示例如下:
// 使用Pick和Exclude組合實(shí)現(xiàn) type Omit<T, K extends keyof T> = Pick<T, Exclude<keyof T, K>>; interface User { id: number; name: string; age: number; gender: number; email: string; } // 表示忽略掉User接口中的age和email屬性 type OmitUser = Omit<User, "age" | "email">; // 等價(jià)于 type OmitUser = { id: number; name: string; gender: number; }; let user: OmitUser = { id: 1, name: 'tom', gender: 1 };
在上述示例中,我們需要忽略掉User接口中的age和email屬性,則只需要將接口名和屬性傳入Omit即可,對(duì)于其他類(lèi)型也是如此,大大提高了類(lèi)型的可擴(kuò)展能力,方便復(fù)用。
總結(jié)
在本文中總結(jié)了幾種TypeScript的使用技巧,如果在我們的TypeScript項(xiàng)目中發(fā)現(xiàn)有很多類(lèi)型聲明的地方具有共性,那么不妨可以使用文中的幾種技巧來(lái)對(duì)其進(jìn)行優(yōu)化改善,增加代碼的可維護(hù)性和可復(fù)用性。筆者之前使用TypeScript的機(jī)會(huì)也不多,所以最近也是一邊學(xué)習(xí)一邊總結(jié),如果文中有錯(cuò)誤的地方,還希望能夠在評(píng)論區(qū)指正。
以上就是這篇文章的全部?jī)?nèi)容了,希望本文的內(nèi)容對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,謝謝大家對(duì)腳本之家的支持。
- TypeScript學(xué)習(xí)之強(qiáng)制類(lèi)型的轉(zhuǎn)換
- 關(guān)于TypeScript中import JSON的正確姿勢(shì)詳解
- 關(guān)于TypeScript模塊導(dǎo)入的那些事
- TypeScript 中接口詳解
- TypeScript Type Innference(類(lèi)型判斷)
- Typescript 中的 interface 和 type 到底有什么區(qū)別詳解
- 深入理解JavaScript和TypeScript中的class
- 如何獲取TypeScript的聲明文件.d.ts
- TypeScript入門(mén)-基本數(shù)據(jù)類(lèi)型
相關(guān)文章
JavaScript清除所有(多個(gè))定時(shí)器的方法實(shí)戰(zhàn)案例
定時(shí)器就是由JS提供了一些原生方法來(lái)實(shí)現(xiàn)延時(shí)去執(zhí)行某一段代碼,下面這篇文章主要給大家介紹了關(guān)于JavaScript清除所有(多個(gè))定時(shí)器的方法,文中通過(guò)代碼介紹的非常詳細(xì),需要的朋友可以參考下2024-01-01JS+CSS實(shí)現(xiàn)的日本門(mén)戶(hù)網(wǎng)站經(jīng)典選項(xiàng)卡導(dǎo)航效果
這篇文章主要介紹了JS+CSS實(shí)現(xiàn)的日本門(mén)戶(hù)網(wǎng)站經(jīng)典選項(xiàng)卡導(dǎo)航效果,涉及JavaScript針對(duì)頁(yè)面元素的動(dòng)態(tài)遍歷及樣式動(dòng)態(tài)修改技巧,具有一定參考借鑒價(jià)值,需要的朋友可以參考下2015-09-09Javascript中常用類(lèi)型的格式化方法小結(jié)
這篇文章主要給大家介紹了Javascript中常用類(lèi)型的格式化方法,其中包括格式化浮點(diǎn)數(shù)、格式化有符號(hào)整數(shù)(int32)、格式化無(wú)符號(hào)整數(shù)(uint32)、格式化布爾值以及格式化字符串等,文中給出了詳細(xì)的示例代碼,有需要的朋友們可以參考借鑒,下面來(lái)一起看看吧。2016-12-12JavaScript實(shí)現(xiàn)身份證驗(yàn)證代碼
本文給大家分享的是使用javascript實(shí)現(xiàn)身份驗(yàn)證的規(guī)則以及代碼,非常的簡(jiǎn)單實(shí)用,有需要的小伙伴可以參考下。2016-02-02Array.prototype.slice.apply的使用方法
arguments在JavaScript語(yǔ)法中是函數(shù)特有的一個(gè)對(duì)象屬性(Arguments對(duì)象),用來(lái)引用調(diào)用該函數(shù)時(shí)傳遞的實(shí)際參數(shù)。2010-03-03javascript之textarea打字機(jī)效果提示代碼推薦
非常不錯(cuò)的提示輸入內(nèi)容,動(dòng)態(tài)的提示,給人親切感2008-09-09關(guān)于 byval 與 byref 的區(qū)別分析總結(jié)
關(guān)于 byval 與 byref 的區(qū)別分析總結(jié)...2007-10-10JavaScript 獲取滾動(dòng)條位置并將頁(yè)面滑動(dòng)到錨點(diǎn)
這篇文章主要介紹了JavaScript 獲取滾動(dòng)條位置并將頁(yè)面滑動(dòng)到錨點(diǎn)的的相關(guān)資料,幫助大家更好的理解和學(xué)習(xí)使用JavaScript,感興趣的朋友可以了解下2021-02-02