TypeScript與JavaScript不同之處系列(八) ===> 高階型別
本系列目的: 列出TypeScript與JavaScript的不同點, 縮小文件內容, 提高學習速度. 原文件地址: https://www.tslang.cn/index.html
這節內容比較多, 但也很實用
文章目錄
高階型別
交叉型別
交叉型別是將多個型別合併為一個型別。 這讓我們可以把現有的多種型別疊加到一起成為一種型別,它包含了所需的所有型別的特性. 例如, Person & Serializable & Loggable
同時是 Person
和 Serializable
Loggable
. 就是說這個型別的物件同時擁有了這三種類型的成員
// 例
function extend<T, U>(first: T, second: U): T & U {
let result = <T & U>{};
for (let id in first) {
(<any>result)[id] = (<any>first)[id];
}
for (let id in second) {
if (!result.hasOwnProperty(id) ) {
(<any>result)[id] = (<any>second)[id];
}
}
return result;
}
class Person {
constructor(public name: string) { }
}
interface Loggable {
log(): void;
}
class ConsoleLogger implements Loggable {
log() {
// ...
}
}
var jim = extend(new Person("Jim"), new ConsoleLogger());
var n = jim.name;
jim.log();
聯合型別(Union Types)
聯合型別表示一個值可以是幾種型別之一. 我們用豎線(|
)分隔每個型別,例如: number | string | boolean
表示一個值可以是 number
, string
,或 boolean
.
// 例
function padLeft(value: string, padding: string | number) {
// ...
}
let indentedString = padLeft("Hello world", true); // error
型別保護
聯合型別適合於那些值可以為不同型別的情況。 但當我們想確切地瞭解是否為某種型別時怎麼辦
interface Bird {
fly();
layEggs();
}
interface Fish {
swim();
layEggs();
}
function getSmallPet(): Fish | Bird {
// ...
return {
fly: () => {console.log(132)},
layEggs: () => {console.log(456)}
}
}
// 普通js可以採用這種方式判斷, 不過在ts裡會報錯
let pet = getSmallPet();
// 每一個成員訪問都會報錯
if (pet.swim) {
pet.swim();
}
else if (pet.fly) {
pet.fly();
}
----------------------------------
// ts裡採用斷言的方式來判斷
let pet = getSmallPet();
if ((<Fish>pet).swim) {
(<Fish>pet).swim();
}
else {
(<Bird>pet).fly();
}
使用者自定義的型別保護
上面的例子中我們不僅在第一個if
使用了斷言而且在else
中也使用了斷言(如果不使用會報錯, Property ‘fly’ does not exist on type ‘Bird | Fish’. Property ‘fly’ does not exist on type ‘Fish’.). 那麼這個自定義型別保護所解決的問題就是: 假若我們一旦檢查過型別,就能在之後的每個分支裡清楚地知道pet
的型別的話就好了.
function isFish(pet: Fish | Bird): pet is Fish {
return (<Fish>pet).swim !== undefined;
}
// 在這個例子裡, pet is Fish就是型別謂詞。 謂詞為 parameterName is Type這種形式, parameterName必須是來自於當前函式簽名裡的一個引數名。
...
if (isFish(pet)) {
pet.swim();
}
else {
pet.fly();
}
注意TypeScript不僅知道在 if分支裡 pet是 Fish型別; 它還清楚在 else分支裡,一定 不是 Fish型別,一定是 Bird型別.
typeof型別保護
typeof型別保護適用於"number"
,"string"
,"boolean"
或 "symbol"
, 當然使用剛剛自定義的型別保護也是可以的, 見下面的兩個例子.
function isNumber(x: any): x is number {
return typeof x === "number";
}
function isString(x: any): x is string {
return typeof x === "string";
}
function padLeft(value: string, padding: string | number) {
if (isNumber(padding)) {
return Array(padding + 1).join(" ") + value;
}
if (isString(padding)) {
return padding + value;
}
throw new Error(`Expected string or number, got '${padding}'.`);
}
----------------------------------------------
// typeof
function padLeft(value: string, padding: string | number) {
if (typeof padding === "number") {
return Array(padding + 1).join(" ") + value;
}
if (typeof padding === "string") {
return padding + value;
}
throw new Error(`Expected string or number, got '${padding}'.`);
}
instanceof型別保護
instanceof
型別保護是通過建構函式來細化型別的一種方式
// 例子
interface Padder {
getPaddingString(): string
}
class SpaceRepeatingPadder implements Padder {
constructor(private numSpaces: number) { }
getPaddingString() {
return Array(this.numSpaces + 1).join(" ");
}
}
class StringPadder implements Padder {
constructor(private value: string) { }
getPaddingString() {
return this.value;
}
}
function getRandomPadder() {
return Math.random() < 0.5 ?
new SpaceRepeatingPadder(4) :
new StringPadder(" ");
}
// 型別為SpaceRepeatingPadder | StringPadder
let padder: Padder = getRandomPadder();
if (padder instanceof SpaceRepeatingPadder) {
padder; // 型別細化為'SpaceRepeatingPadder'
}
if (padder instanceof StringPadder) {
padder; // 型別細化為'StringPadder'
}
判斷null
這與在JavaScript裡寫的程式碼一致:
function f(sn: string | null): string {
if (sn == null) {
return "default";
}
else {
return sn;
}
}
-------------或者-------------------
function f(sn: string | null): string {
return sn || "default";
}
null型別
TypeScript具有兩種特殊的型別, null
和 undefined
. 預設情況下,型別檢查器認為 null與 undefined可以賦值給任何型別. 如果想取消預設行為可以在編譯時在命令後面加一個--strictNullChecks
, 例如: tsc 檔名 --strictNullChecks
let s = "foo";
s = null; // 錯誤, 'null'不能賦值給'string'
let sn: string | null = "bar";
sn = null; // 可以
sn = undefined; // error, 'undefined'不能賦值給'string | null', ts會把 null和 undefined區別對待
可選引數和可選屬性
使用了 --strictNullChecks
,可選引數會被自動地加上 | undefined
:
// 可選引數
function f(x: number, y?: number) {
return x + (y || 0);
}
f(1, 2);
f(1);
f(1, undefined);
f(1, null); // error, 'null' is not assignable to 'number | undefined'
------------------------------------------------
// 可選屬性
class C {
a: number;
b?: number;
}
let c = new C();
c.a = 12;
c.a = undefined; // error, 'undefined' is not assignable to 'number'
c.b = 13;
c.b = undefined; // ok
c.b = null; // error, 'null' is not assignable to 'number | undefined'
類型別名
類型別名會給一個型別起個新名字。 類型別名有時和介面很像,但是可以作用於原始值,聯合型別,元組以及其它任何你需要手寫的型別. 起別名不會新建一個型別 - 它建立了一個新 名字來引用那個型別, 使用type
關鍵字宣告別名.
// 例1
type Name = string;
type NameResolver = () => string;
type NameOrResolver = Name | NameResolver;
function getName(n: NameOrResolver): Name {
if (typeof n === 'string') {
return n;
}
else {
return n();
}
}
// 例2 -- 泛型
type Container<T> = { value: T };
// 例3 -- 引用自己
type Tree<T> = {
value: T;
left: Tree<T>;
right: Tree<T>;
}
// 例4 -- 與交叉型別一起使用
type LinkedList<T> = T & { next: LinkedList<T> };
interface Person {
name: string;
}
let people: LinkedList<Person>;
let s = people.name;
let s = people.next.name;
// 例5 --- 注: 類型別名不能出現在宣告右側的任何地方
type Yikes = Array<Yikes>; // error
介面 vs. 類型別名
注意上面提到的是, 介面和類型別名 有時 很像, 那麼他們的區別是什麼呢.
1,介面建立了一個新的名字,可以在其它任何地方使用。 類型別名並不建立新名字—比如,錯誤資訊就不會使用別名。 在下面的示例程式碼裡,在編譯器中將滑鼠懸停在interfaced
上,顯示它返回的是Interface
,但懸停在 aliased
上時,顯示的卻是物件字面量型別。
type Alias = { num: number }
interface Interface {
num: number;
}
declare function aliased(arg: Alias): Alias;
declare function interfaced(arg: Interface): Interface;
2, 類型別名不能被 extends和 implements(自己也不能 extends和 implements其它型別)。 因為 軟體中的物件應該對於擴充套件是開放的,但是對於修改是封閉的,你應該儘量去使用介面代替類型別名.
總結: 如果你無法通過介面來描述一個型別並且需要使用聯合型別或元組型別,這時通常會使用類型別名.
字串字面量型別
字串字面量型別允許你指定字串必須的固定值。 在實際應用中,字串字面量型別可以與聯合型別,型別保護和類型別名很好的配合。 通過結合使用這些特性,你可以實現類似列舉型別的字串.
type Easing = "ease-in" | "ease-out" | "ease-in-out";
class UIElement {
animate(dx: number, dy: number, easing: Easing) {
if (easing === "ease-in") {
// ...
}
else if (easing === "ease-out") {
}
else if (easing === "ease-in-out") {
}
else {
// ...
}
}
}
let button = new UIElement();
button.animate(0, 0, "ease-in");
button.animate(0, 0, "uneasy"); // error: "uneasy"不在type型別裡面
字串字面量型別還可以用於區分函式過載:
function createElement(tagName: "img"): HTMLImageElement;
function createElement(tagName: "input"): HTMLInputElement;
// ... more overloads ...
function createElement(tagName: string): Element {
// ... code goes here ...
}
數字字面量型別
TypeScript
還具有數字字面量型別。
function rollDie(): 1 | 2 | 3 | 4 | 5 | 6 {
// ...
}
可辨識型別
它具有3個要素:
- 具有普通的單例型別屬性— 可辨識的特徵
- 一個類型別名包含了那些型別的聯合— 聯合
- 此屬性上的型別保護
interface Square {
kind: "square";
size: number;
}
interface Rectangle {
kind: "rectangle";
width: number;
height: number;
}
interface Circle {
kind: "circle";
radius: number;
}
// 首先我們聲明瞭將要聯合的介面。 每個介面都有 kind屬性但有不同的字串字面量型別。 kind屬性稱做 可辨識的特徵或 標籤。 其它的屬性則特定於各個介面。 注意,目前各個介面間是沒有聯絡的。 下面我們把它們聯合到一起:
type Shape = Square | Rectangle | Circle;
// 現在我們使用可辨識聯合:
function area(s: Shape) {
switch (s.kind) {
case "square": return s.size * s.size;
case "rectangle": return s.height * s.width;
case "circle": return Math.PI * s.radius ** 2;
}
}
完整性檢查
此節目的: 當沒有涵蓋所有可辨識聯合的變化時,我們想讓編譯器可以通知我們。 比如,如果我們添加了 Triangle到 Shape,我們同時還想編譯器通知我更新 area:
第一種方法, 首先是啟用 --strictNullChecks並且指定一個返回值型別(上面有提到過, 如果開啟了這個引數, 就不能隨便把null和undefined進行賦值)
function area(s: Shape): number { // error: returns number | undefined
switch (s.kind) {
case "square": return s.size * s.size;
case "rectangle": return s.height * s.width;
case "circle": return Math.PI * s.radius ** 2;
}
}
第二種方法, 使用 never型別
// assertNever檢查 s是否為 never型別—即為除去所有可能情況後剩下的型別
function assertNever(x: never): never {
throw new Error("Unexpected object: " + x);
}
function area(s: Shape) {
switch (s.kind) {
case "square": return s.size * s.size;
case "rectangle": return s.height * s.width;
case "circle": return Math.PI * s.radius ** 2;
default: return assertNever(s); // error here if there are missing cases
}
}
多型的 this型別
多型的 this型別表示的是某個包含類或介面的子型別
class BasicCalculator {
public constructor(protected value: number = 0) { }
public currentValue(): number {
return this.value;
}
public add(operand: number): this {
this.value += operand;
return this;
}
public multiply(operand: number): this {
this.value *= operand;
return this;
}
// ... other operations go here ...
}
let v = new