TypeScript學習筆記(二)—— TypeScript基礎

一、原始數據類型基本使用

TypeScript中擁有更多的類型,如下表所示:

類型例子描述
number 1, -33, 2.5 任意數字
string 'hi', "hi", hi 任意字符串
boolean true、false 布爾值true或false
字面量 其本身 限制變量的值就是該字面量的值
any * 任意類型
unknown * 類型安全的any
void 空值(undefined) 沒有值(或undefined)
never 沒有值 不能是任何值
object {name:'孫悟空'} 任意的JS對象
array [1,2,3] 任意JS數組
tuple [4,5] 元素,TS新增類型,固定長度數組
enum enum{A, B} 枚舉,TS中新增類型

JavaScript 的類型分爲兩種:原始數據類型(Primitive data types)和對象類型(Object types)。

原始數據類型包括:布爾值、數值、字符串、null、undefined 以及 ES6 中的新類型 Symbol

本節主要介紹前五種原始數據類型在 TypeScript 中的應用。

javascript原始類型:布爾值、數值、字符串、null、undefined,爲變量指定類型,且變量值需與類型一致

let flag: boolean = false
let num: number = 15
let str: string = 'abc'
var a: null = null
var b: undefined = undefined

// 編譯通過

使用構造函數創造的對象不是原始數據類型,事實上 new XXX() 返回的是一個 XXX對象

let flag:boolean=new Boolean(false)  // Type 'Boolean' is not assignable to type 'boolean'.
let num: number = new Number(15)     // Type 'Number' is not assignable to type 'number'.
let str: string = new String('abc')  // Type 'String' is not assignable to type 'string'.

// 編譯通過

但是可以直接調用 XXX 也可以返回一個 xxx 類型

  let flag: boolean = Boolean(false)
  let num : number  = Number(15)
  let str : string  = String('abc')
  // 編譯通過
//javascript 中的數據類型分爲:原始數據類型 與 對象數據類型
//原始數據類型:number,string,boolean,null,undefined,symbol,bigInt
//對象數據類型:Array,Object,RegExp...
let n:number=1;
let s:string="hello";
let b:boolean=true;
let nu:null=null;
let und:undefined=undefined;
let sy:symbol=Symbol();
let o:Object={};

let num:number;

//num=new Number(100);  //錯誤,new Number實例化生成的數據類型是Object類型
//new String("");  //Object類型
//new Boolean("true");  //Object類型


let a:number;
a=Number(200);  //直接調用函數Number返回的是number類型的數據

let c:string;
c=String("hi");  //直接調用String函數返回的是string類型的數據

let d:boolean;
d=Boolean(false);  //直接調用Boolean函數返回的是boolean類型的數據

二、原始數據類型

JavaScript 的類型分爲兩種:原始數據類型(Primitive data types)和對象類型(Object types)。

原始數據類型包括:布爾值、數值、字符串、nullundefined 以及 ES6 中的新類型 Symbol 和 ES10 中的新類型 BigInt

本節主要介紹前五種原始數據類型在 TypeScript 中的應用。

2.1、布爾值

布爾值是最基礎的數據類型,在 TypeScript 中,使用 boolean 定義布爾值類型:

let isDone: boolean = false;

// 編譯通過
// 後面約定,未強調編譯錯誤的代碼片段,默認爲編譯通過

注意,使用構造函數 Boolean 創造的對象不是布爾值:

let createdByNewBoolean: boolean = new Boolean(1);

// Type 'Boolean' is not assignable to type 'boolean'.
//   'boolean' is a primitive, but 'Boolean' is a wrapper object. Prefer using 'boolean' when possible.

事實上 new Boolean() 返回的是一個 Boolean 對象:

let createdByNewBoolean: Boolean = new Boolean(1);

直接調用 Boolean 也可以返回一個 boolean 類型:

let createdByBoolean: boolean = Boolean(1);

在 TypeScript 中,boolean 是 JavaScript 中的基本類型,而 Boolean 是 JavaScript 中的構造函數。其他基本類型(除了 null 和 undefined)一樣,不再贅述。

let isShow=true;  //推斷類型,isShow根據值被編譯器推斷爲boolean類型

//isShow=1;  //錯誤不能將類型“number”分配給類型“boolean”。ts(2322)

//isShow=new Boolean(true);  //錯誤,返回的是Object

isShow=Boolean(1);  //正確的,返回的是boolean類型的false

isShow=Boolean(0);   //false

isShow=Boolean(undefined); //false
 
isShow=Boolean(null); //false

isShow=Boolean(""); //false

isShow=Boolean({}); //true

console.log(isShow); 

2.2、數值

使用 number 定義數值類型:

let decLiteral: number = 6;
let hexLiteral: number = 0xf00d;
// ES6 中的二進制表示法
let binaryLiteral: number = 0b1010;
// ES6 中的八進制表示法
let octalLiteral: number = 0o744;
let notANumber: number = NaN;
let infinityNumber: number = Infinity;

編譯結果:

var decLiteral = 6;
var hexLiteral = 0xf00d;
// ES6 中的二進制表示法
var binaryLiteral = 10;
// ES6 中的八進制表示法
var octalLiteral = 484;
var notANumber = NaN;
var infinityNumber = Infinity;

其中 0b1010 和 0o744 是 ES6 中的二進制和八進制表示法,它們會被編譯爲十進制數字。

//數值 number Number

let n1:number=10;  //十進制
let n2:number=0xFFF;  //十六進制
let n3:number=0o176;  //八進制
let n4:number=0b1101; //二進制
let n5:number=15.6;  //小數,浮點數
let n6:number=NaN; //非數字
let n7:number=Infinity; //無限大

//n1=new Number(100);  //錯誤,返回的Object類型

n1=Number("200");  //正確

console.log(n1);  //200

2.3、字符串

使用 string 定義字符串類型:

let myName: string = 'Tom';
let myAge: number = 25;

// 模板字符串
let sentence: string = `Hello, my name is ${myName}.
I'll be ${myAge + 1} years old next month.`;

編譯結果:

var myName = 'Tom';
var myAge = 25;
// 模板字符串
var sentence = "Hello, my name is " + myName + ".
I'll be " + (myAge + 1) + " years old next month.";

其中 ` 用來定義 ES6 中的模板字符串${expr} 用來在模板字符串中嵌入表達式。

//字符串 string String

let studentname:string="tom";
let studentage:number=18;

let result=`我叫${studentname},明年${++studentage}歲`;  
//result=100;  //錯誤,因爲模板字符串被推斷爲string數據類型
console.log(result);  //我叫tom,明年19歲

let a=new String("hello world");  
console.log(typeof a);  //object

//let b:string=new String("hello world"); //不能將類型“String”分配給類型“string”。 object

let c:string=String(true);
console.log(c);  //true

2.4、空值

JavaScript 沒有空值(Void)的概念,在 TypeScript 中,可以用 void 表示沒有任何返回值的函數:

function alertName(): void {
    alert('My name is Tom');
}

聲明一個 void 類型的變量沒有什麼用,因爲你只能將它賦值爲 undefined 和 null(只在 --strictNullChecks 未指定時):

let unusable: void = undefined;
//void 空值

let a:void=undefined;  //一般不會使用在變量類型的定義中

//a=null;  //默認錯誤


function f1():void{  //表明函數沒有返回值
    //return 100;  //不允許
}

2.5、Null 和 Undefined

在 TypeScript 中,可以使用 null 和 undefined 來定義這兩個原始數據類型:

let u: undefined = undefined;
let n: null = null;
//null,undefined

let n1:null=null;
//n1=undefined;  //錯誤

let n2:undefined=undefined;
//n2=null;  //錯誤

2.6、Infinity無窮大

Infinity(無窮大)在 JS 中是一個特殊的數字,它的特性是:它比任何有限的數字都大,在 Javascript 中,超出 1.797693134862315E+308 的數值即爲 Infinity,小於 -1.797693134862316E+308 的數值爲無窮小。

1.Infinity(無窮)的定義

無窮可以分爲兩種,正無窮和負無窮,JS 中對應的表示方式爲:+Infinity(或者Infinity) 和 -Infinity。

這意味着Infinity和-Infinity(小於任何有限數的數字)都是number類型的特殊值:

typeof Infinity; // => 'number'
typeof -Infinity; // => 'number'

Infinity 是全局對象的屬性:

window.Infinity; // => Infinity

另外,Number函數也有兩個屬性來表示正負無窮大:

Number.POSITIVE_INFINITY; // => Infinity
Number.NEGATIVE_INFINITY; // => -Infinity

2. Infinity 的特性

Infinity比任何有限數都大。

舉幾個例子 :

Infinity > 100;                     // => true
Infinity > Number.MAX_SAFE_INTEGER; // => true
Infinity > Number.MAX_VALUE;        // => true

Number.MAX_SAFE_INTEGER 常量表示在 JavaScript 中最大的安全整數(maxinum safe integer)(2^53 - 1),數值,9007199254740991。

Infinity 在加法、乘法和除法等算術運算中用作操作數時會產生有趣的效果:

Infinity + 1;        // => Infinity
Infinity + Infinity; // => Infinity

Infinity * 2;        // => Infinity
Infinity * Infinity; // => Infinity

Infinity / 2;        // => Infinity

一些Infinity 的運算得到有限的數:

10 / Infinity; // => 0

一個有限的數除以0得到 Infinity 結果:

2 / 0; // => Infinity

對無窮數進行概念上不正確的運算會得到NaN。 例如,不能除以無限數,也無法確定無限數是奇數還是偶數:

Infinity / Infinity; // => NaN
Infinity % 2;        // => NaN

2.1 負無窮

負無窮小於任何有限數。

將-Infinity 與一些有限數字進行比較:

-Infinity < 100;                      // => true
-Infinity < -Number.MAX_SAFE_INTEGER; // => true
-Infinity < -Number.MAX_VALUE;        // => true

同時,負無窮小於正無窮:

-Infinity < Infinity; // => true

當使用不同操作符操作數時,也可能會得到負無窮:

Infinity * -1; // => -Infinity
Infinity / -2; // => -Infinity
-2 / 0;        // => -Infinity

3.判斷無窮

幸運的是,Infinity等於相同符號的Infinity:

Infinity === Infinity; // => true
-Infinity === -Infinity; // => true

但前面的符號不一樣就不相等,就也很好理解:

Infinity === -Infinity; // => false

JSt有一個特殊的函數Number.isFinite(value),用於檢查提供的值是否有限數:

Number.isFinite(Infinity);  // => false
Number.isFinite(-Infinity); // => false
Number.isFinite(999);       // => true

2.7、Number的屬性

屬性描述
constructor 返回對創建對象的Number函數的引用
MAX_VALUE 返回JavaScript中最大數字
MIN_VALUE 返回JavaScript中最小數字
MAX_SAFE_INTEGER 返回JavaScript中的最大安全整數(2 53-1)
MIN_SAFE_INTEGER 返回在JavaScript中最小安全整數(-2 53 - 1)
NaN 代表“非數字”值
NEGATIVE_INFINITY 表示負無窮大(溢出時返回)
POSITIVE_INFINITY 表示正無窮大(溢出時返回)
prototype 允許您向對象添加屬性和方法

 

三、任意值

typescript中,當我們不確定一個類型是什麼類型的,可以選擇給其聲明爲any或者unkown。但實際上,typescript推薦使用unknown,因爲unknown是類型安全的。

任意值(Any)用來表示允許賦值爲任意類型。

3.1、什麼是任意值類型

如果是一個普通類型,在賦值過程中改變類型是不被允許的:

let myFavoriteNumber: string = 'seven';
myFavoriteNumber = 7;

// index.ts(2,1): error TS2322: Type 'number' is not assignable to type 'string'.

但如果是 any 類型,則允許被賦值爲任意類型。

let myFavoriteNumber: any = 'seven';
myFavoriteNumber = 7;

3.2、任意值的屬性和方法

在任意值上訪問任何屬性都是允許的:

let anyThing: any = 'hello';
console.log(anyThing.myName);  //undefined

可以認爲,聲明一個變量爲任意值之後,對它的任何操作,返回的內容的類型都是任意值。

3.3、未聲明類型的變量

變量如果在聲明的時候,未指定其類型,那麼它會被識別爲任意值類型:

let something;
something = 'seven';
something = 7;

something.setName('Tom');

等價於

let something: any;
something = 'seven';
something = 7;

something.setName('Tom');

3.4、任意值可以賦值給已知類型變量

let a:number;
a=100;

let b:any;
b=200;
b="hello world";

a=b;

console.log(typeof a);

結果

let a:any;

//可以將任意類型賦值給any類型
a=7;
a="hi";

//任意類型可以接收any類型的值
let b:boolean;
b=true;

b=a;  //注意這裏是正確的,b的類型已改變成a的類型string,注意不是any

//b=8;  //錯誤

console.log(typeof b); //string

//如果在定義變量時沒有指定類型,則變量爲any類型
let c;  //這裏相當於 let c:any
function f1(a,b,c){  //function f1(a: any, b: any, c: any): void
}

3.5、unknown

  • let notSure: unknown = 4;
    notSure = 'hello';

any是任意類型的父類型,同時也任意類型的子類型
unknown是任意類型的父類型,但僅此而已。

let a:unknown;
let b:number;

a=1;
a="hello world";

b=a;  //錯誤

示例2

let a:unknown;

//unknown類型可以被賦值爲任意類型這點與any類似

a=7;
a=true;
a="hello";

//但是其它類型不能接收unknown的值,不能將unknown賦值給其它類
let b:boolean;
b=true;

b=a;  //錯誤,如果a是any類型則允許

3.6、nerver

  • function error(message: string): never {
      throw new Error(message);
    }

永遠都不會有返回的函數:

1、函數拋出異常

function a () {
    throw TypeError('this is a error')
}

2、永遠執行下去的函數

function b() {
    while(true) {
    // todo
    }
}

四、類型推論

如果沒有明確的指定類型,那麼 TypeScript 會依照類型推論(Type Inference)的規則推斷出一個類型。

以下代碼雖然沒有指定類型,但是會在編譯的時候報錯:

let myFavoriteNumber = 'seven';
myFavoriteNumber = 7;

// index.ts(2,1): error TS2322: Type 'number' is not assignable to type 'string'.

事實上,它等價於:

let myFavoriteNumber: string = 'seven';
myFavoriteNumber = 7;

// index.ts(2,1): error TS2322: Type 'number' is not assignable to type 'string'.

TypeScript 會在沒有明確的指定類型的時候推測出一個類型,這就是類型推論。

如果定義的時候沒有賦值,不管之後有沒有賦值,都會被推斷成 any 類型而完全不被類型檢查:

let myFavoriteNumber;
myFavoriteNumber = 'seven';
myFavoriteNumber = 7;

let a=100;  //a被推斷爲number 等同於let a:number=100;

let b=true; //b被推斷爲boolean類型

let c;

c="hello";

console.log(typeof c);  //string

function f1(){  //函數的返回值被推斷爲number,相當於f1():number
    return 1;
}

五、聯合類型

聯合類型(Union Types)表示取值可以爲多種類型中的一種。

5.1、簡單的例子

let myFavoriteNumber: string | number;
myFavoriteNumber = 'seven';
myFavoriteNumber = 7;
let myFavoriteNumber: string | number;
myFavoriteNumber = true;

// index.ts(2,1): error TS2322: Type 'boolean' is not assignable to type 'string | number'.
//   Type 'boolean' is not assignable to type 'number'.

聯合類型使用 | 分隔每個類型。

這裏的 let myFavoriteNumber: string | number 的含義是,允許 myFavoriteNumber 的類型是 string 或者 number,但是不能是其他類型。

5.2、訪問聯合類型的屬性或方法

當 TypeScript 不確定一個聯合類型的變量到底是哪個類型的時候,我們只能訪問此聯合類型的所有類型裏共有的屬性或方法:

function getLength(something: string | number): number {
    return something.length;
}

// index.ts(2,22): error TS2339: Property 'length' does not exist on type 'string | number'.
//   Property 'length' does not exist on type 'number'.

上例中,length 不是 string 和 number 的共有屬性,所以會報錯。

訪問 string 和 number 的共有屬性是沒問題的:

function getString(something: string | number): string {
    return something.toString();
}

聯合類型的變量在被賦值的時候,會根據類型推論的規則推斷出一個類型:

let myFavoriteNumber: string | number;
myFavoriteNumber = 'seven';
console.log(myFavoriteNumber.length); // 5
myFavoriteNumber = 7;
console.log(myFavoriteNumber.length); // 編譯時報錯

// index.ts(5,30): error TS2339: Property 'length' does not exist on type 'number'.

上例中,第二行的 myFavoriteNumber 被推斷成了 string,訪問它的 length 屬性不會報錯。

而第四行的 myFavoriteNumber 被推斷成了 number,訪問它的 length 屬性時就報錯了。

let a:string | number | boolean;

a="hello";
a=true;
a=666;

//a={};  //錯誤,對象類型

console.log(typeof a);  //number

function f1(c:string|number){  //函數的參數可以是聯合類型
}

f1(100);
f1("abc");

六、對象的類型——接口

在 TypeScript 中,我們使用接口(Interfaces)來定義對象的類型。

6.1、什麼是接口

在面嚮對象語言中,接口(Interfaces)是一個很重要的概念,它是對行爲的抽象,而具體如何行動需要由類(classes)去實現(implement)。

TypeScript 中的接口是一個非常靈活的概念,除了可用於對類的一部分行爲進行抽象以外,也常用於對「對象的形狀(Shape)」進行描述。

6.2、簡單的例子

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

let tom: Person = {
    name: 'Tom',
    age: 25
};

上面的例子中,我們定義了一個接口 Person,接着定義了一個變量 tom,它的類型是 Person。這樣,我們就約束了 tom 的形狀必須和接口 Person 一致。

接口一般首字母大寫。有的編程語言中會建議接口的名稱加上 I 前綴

定義的變量比接口少了一些屬性是不允許的:

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

let tom: Person = {
    name: 'Tom'
};

// index.ts(6,5): error TS2322: Type '{ name: string; }' is not assignable to type 'Person'.
//   Property 'age' is missing in type '{ name: string; }'.

多一些屬性也是不允許的:

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

let tom: Person = {
    name: 'Tom',
    age: 25,
    gender: 'male'
};

// index.ts(9,5): error TS2322: Type '{ name: string; age: number; gender: string; }' is not assignable to type 'Person'.
//   Object literal may only specify known properties, and 'gender' does not exist in type 'Person'.

可見,賦值的時候,變量的形狀必須和接口的形狀保持一致。

6.3、可選屬性

有時我們希望不要完全匹配一個形狀,那麼可以用可選屬性:

interface Person {
    name: string;
    age?: number;
}

let tom: Person = {
    name: 'Tom'
};
interface Person {
    name: string;
    age?: number;
}

let tom: Person = {
    name: 'Tom',
    age: 25
};

可選屬性的含義是該屬性可以不存在。

這時仍然不允許添加未定義的屬性:

interface Person {
    name: string;
    age?: number;
}

let tom: Person = {
    name: 'Tom',
    age: 25,
    gender: 'male'
};

// examples/playground/index.ts(9,5): error TS2322: Type '{ name: string; age: number; gender: string; }' is not assignable to type 'Person'.
//   Object literal may only specify known properties, and 'gender' does not exist in type 'Person'.

6.4、任意屬性

有時候我們希望一個接口允許有任意的屬性,可以使用如下方式:

interface Person {
    name: string;
    age?: number;
    [propName: string]: any;
}

let tom: Person = {
    name: 'Tom',
    gender: 'male'
};

使用 [propName: string] 定義了任意屬性取 string 類型的值。

需要注意的是,一旦定義了任意屬性,那麼確定屬性和可選屬性的類型都必須是它的類型的子集:

interface Person {
    name: string;
    age?: number;
    [propName: string]: string;
}

let tom: Person = {
    name: 'Tom',
    age: 25,
    gender: 'male'
};

// index.ts(3,5): error TS2411: Property 'age' of type 'number' is not assignable to string index type 'string'.
// index.ts(7,5): error TS2322: Type '{ [x: string]: string | number; name: string; age: number; gender: string; }' is not assignable to type 'Person'.
//   Index signatures are incompatible.
//     Type 'string | number' is not assignable to type 'string'.
//       Type 'number' is not assignable to type 'string'.

上例中,任意屬性的值允許是 string,但是可選屬性 age 的值卻是 numbernumber 不是 string 的子屬性,所以報錯了。

另外,在報錯信息中可以看出,此時 { name: 'Tom', age: 25, gender: 'male' } 的類型被推斷成了 { [x: string]: string | number; name: string; age: number; gender: string; },這是聯合類型和接口的結合。

一個接口中只能定義一個任意屬性。如果接口中有多個類型的屬性,則可以在任意屬性中使用聯合類型:

interface Person {
    name: string;
    age?: number;
    [propName: string]: string | number;
}

let tom: Person = {
    name: 'Tom',
    age: 25,
    gender: 'male'
};

6.5、只讀屬性

有時候我們希望對象中的一些字段只能在創建的時候被賦值,那麼可以用 readonly 定義只讀屬性:

interface Person {
    readonly id: number;
    name: string;
    age?: number;
    [propName: string]: any;
}

let tom: Person = {
    id: 89757,
    name: 'Tom',
    gender: 'male'
};

tom.id = 9527;

// index.ts(14,5): error TS2540: Cannot assign to 'id' because it is a constant or a read-only property.

上例中,使用 readonly 定義的屬性 id 初始化後,又被賦值了,所以報錯了。

注意,只讀的約束存在於第一次給對象賦值的時候,而不是第一次給只讀屬性賦值的時候:

interface Person {
    readonly id: number;
    name: string;
    age?: number;
    [propName: string]: any;
}

let tom: Person = {
    name: 'Tom',
    gender: 'male'
};

tom.id = 89757;

// index.ts(8,5): error TS2322: Type '{ name: string; gender: string; }' is not assignable to type 'Person'.
//   Property 'id' is missing in type '{ name: string; gender: string; }'.
// index.ts(13,5): error TS2540: Cannot assign to 'id' because it is a constant or a read-only property.

上例中,報錯信息有兩處,第一處是在對 tom 進行賦值的時候,沒有給 id 賦值。

第二處是在給 tom.id 賦值的時候,由於它是隻讀屬性,所以報錯了。

6.6、方法

interface IPerson { 
    firstName:string, 
    lastName:string, 
    sayHi: ()=>string 
} 
 
var customer:IPerson = { 
    firstName:"Tom",
    lastName:"Hanks", 
    sayHi: ():string =>{return "Hi there"} 
}

console.log("Customer 對象 ");
console.log(customer.firstName); 
console.log(customer.lastName); 
console.log(customer.sayHi());

在接口中定義了方法sayHi,要求返回值爲String類型。

運行結果:

Customer 對象
Tom
Hanks
Hi there

編譯以後的代碼:

var customer = {
    firstName: "Tom",
    lastName: "Hanks",
    sayHi: function () { return "Hi there"; }
};
console.log("Customer 對象 ");
console.log(customer.firstName);
console.log(customer.lastName);
console.log(customer.sayHi());

6.7、聯合類型和接口

以下實例演示瞭如何在接口中使用聯合類型:

interface RunOptions { 
    program:string; 
    commandline:string[]|string|(()=>string); 
} 
 
// commandline 是字符串
var options:RunOptions = {program:"test1",commandline:"Hello"}; 
console.log(options.commandline)  
 
// commandline 是字符串數組
options = {program:"test1",commandline:["Hello","World"]}; 
console.log(options.commandline[0]); 
console.log(options.commandline[1]);  
 
// commandline 是一個函數表達式
options = {program:"test1",commandline:()=>{return "**Hello World**";}}; 
 
var fn:any = options.commandline; 
console.log(fn());

編譯以上代碼,得到以下 JavaScript 代碼:

// commandline 是字符串
var options = { program: "test1", commandline: "Hello" };
console.log(options.commandline);
// commandline 是字符串數組
options = { program: "test1", commandline: ["Hello", "World"] };
console.log(options.commandline[0]);
console.log(options.commandline[1]);
// commandline 是一個函數表達式
options = { program: "test1", commandline: function () { return "**Hello World**"; } };
var fn = options.commandline;
console.log(fn());

6.8、接口和數組

接口中我們可以將數組的索引值和元素設置爲不同類型,索引值可以是數字或字符串。

設置元素爲字符串類型:

示例1:

interface namelist { 
    [index:number]:string 
 } 
  
 // 類型一致,正確
 var list2:namelist = ["Google","Runoob","Taobao"]
 // 錯誤元素 1 不是 string 類型
 // var list2:namelist = ["Runoob",1,"Taobao"]

示例2:

interface ages { 
    [index:string]:number 
 } 
  
 var agelist:ages; 
  // 類型正確 
 agelist["runoob"] = 15  
  
 // 類型錯誤,輸出  error TS2322: Type '"google"' is not assignable to type 'number'.
 // agelist[2] = "google"

6.9、接口繼承

接口繼承就是說接口可以通過其他接口來擴展自己。

Typescript 允許接口繼承多個接口。

繼承使用關鍵字 extends。

單接口繼承語法格式:

Child_interface_name extends super_interface_name

多接口繼承語法格式:

Child_interface_name extends super_interface1_name, super_interface2_name,…,super_interfaceN_name
繼承的各個接口使用逗號 , 分隔。

單繼承實例

interface Person { 
   age:number 
} 
 
interface Musician extends Person { 
   instrument:string 
} 
 
var drummer = <Musician>{}; 
drummer.age = 27 
drummer.instrument = "Drums" 
console.log("年齡:  "+drummer.age)
console.log("喜歡的樂器:  "+drummer.instrument)

輸出結果爲:

年齡:  27
喜歡的樂器:  Drums

多繼承實例

interface IParent1 { 
    v1:number 
} 
 
interface IParent2 { 
    v2:number 
} 
 
interface Child extends IParent1, IParent2 { } 
var Iobj:Child = { v1:12, v2:23} 
console.log("value 1: "+Iobj.v1+" value 2: "+Iobj.v2)

輸出結果爲:

value 1: 12 value 2: 23

6.10、匿名接口

可以爲對象指定一個匿名接口,如下所示:

let obje:{a:string,b:number}={a:"a",b:1};

七、數組的類型

在 TypeScript 中,數組類型有多種定義方式,比較靈活。

7.1、「類型 + 方括號」表示法

最簡單的方法是使用「類型 + 方括號」來表示數組:

let fibonacci: number[] = [1, 1, 2, 3, 5];

數組的項中不允許出現其他的類型:

let fibonacci: number[] = [1, '1', 2, 3, 5];

// Type 'string' is not assignable to type 'number'.

數組的一些方法的參數也會根據數組在定義時約定的類型進行限制:

let fibonacci: number[] = [1, 1, 2, 3, 5];
fibonacci.push('8');

// Argument of type '"8"' is not assignable to parameter of type 'number'.

上例中,push 方法只允許傳入 number 類型的參數,但是卻傳了一個 "8" 類型的參數,所以報錯了。這裏 "8" 是一個字符串字面量類型,會在後續章節中詳細介紹。

7.2、數組泛型

我們也可以使用數組泛型(Array Generic) Array<elemType> 來表示數組:

let fibonacci: Array<number> = [1, 1, 2, 3, 5];

7.3、用接口表示數組

接口也可以用來描述數組:

interface NumberArray {
    [index: number]: number;
}
let fibonacci: NumberArray = [1, 1, 2, 3, 5];

NumberArray 表示:只要索引的類型是數字時,那麼值的類型必須是數字。

雖然接口也可以用來描述數組,但是我們一般不會這麼做,因爲這種方式比前兩種方式複雜多了。

不過有一種情況例外,那就是它常用來表示類數組。

7.4、類數組

類數組(Array-like Object)不是數組類型,比如 arguments

function sum() {
    let args: number[] = arguments;
}

// Type 'IArguments' is missing the following properties from type 'number[]': pop, push, concat, join, and 24 more.

上例中,arguments 實際上是一個類數組,不能用普通的數組的方式來描述,而應該用接口:

function sum() {
    let args: {
        [index: number]: number;
        length: number;
        callee: Function;
    } = arguments;
}

在這個例子中,我們除了約束當索引的類型是數字時,值的類型必須是數字之外,也約束了它還有 length 和 callee 兩個屬性。

事實上常用的類數組都有自己的接口定義,如 IArgumentsNodeListHTMLCollection 等:

function sum() {
    let args: IArguments = arguments;
}

其中 IArguments 是 TypeScript 中定義好了的類型,它實際上就是:

interface IArguments {
    [index: number]: any;
    length: number;
    callee: Function;
}

7.5、any 在數組中的應用

一個比較常見的做法是,用 any 表示數組中允許出現任意類型:

let list: any[] = ['xcatliu', 25, { website: 'http://abc.com' }];

八、函數的類型

8.1、函數聲明

在 JavaScript 中,有兩種常見的定義函數的方式——函數聲明(Function Declaration)和函數表達式(Function Expression):

// 函數聲明(Function Declaration)
function sum(x, y) {
    return x + y;
}

// 函數表達式(Function Expression)
let mySum = function (x, y) {
    return x + y;
};

一個函數有輸入和輸出,要在 TypeScript 中對其進行約束,需要把輸入和輸出都考慮到,其中函數聲明的類型定義較簡單:

function sum(x: number, y: number): number {
    return x + y;
}

注意,輸入多餘的(或者少於要求的)參數,是不被允許的:

function sum(x: number, y: number): number {
    return x + y;
}
sum(1, 2, 3);

// index.ts(4,1): error TS2346: Supplied parameters do not match any signature of call target.
function sum(x: number, y: number): number {
    return x + y;
}
sum(1);

// index.ts(4,1): error TS2346: Supplied parameters do not match any signature of call target.

8.2、函數表達式

如果要我們現在寫一個對函數表達式(Function Expression)的定義,可能會寫成這樣:

let mySum = function (x: number, y: number): number {
    return x + y;
};

這是可以通過編譯的,不過事實上,上面的代碼只對等號右側的匿名函數進行了類型定義,而等號左邊的 mySum,是通過賦值操作進行類型推論而推斷出來的。如果需要我們手動給 mySum 添加類型,則應該是這樣:

let mySum: (x: number, y: number) => number = function (x: number, y: number): number {
    return x + y;
};

注意不要混淆了 TypeScript 中的 => 和 ES6 中的 =>

在 TypeScript 的類型定義中,=> 用來表示函數的定義,左邊是輸入類型,需要用括號括起來,右邊是輸出類型。

在 ES6 中,=> 叫做箭頭函數,應用十分廣泛,可以參考 ES6 中的箭頭函數

mySum: (x: number, y: number) => number 是對函數的參數與返回值的約束
function (x: number, y: number): number是對函數約束的實現

8.3、用接口定義函數的形狀

我們也可以使用接口的方式來定義一個函數需要符合的形狀:

interface SearchFunc {
    (source: string, subString: string): boolean;
}

let mySearch: SearchFunc;
mySearch = function(source: string, subString: string) {
    return source.search(subString) !== -1;
}

採用函數表達式|接口定義函數的方式時,對等號左側進行類型限制,可以保證以後對函數名賦值時保證參數個數、參數類型、返回值類型不變。

8.4、可選參數

前面提到,輸入多餘的(或者少於要求的)參數,是不允許的。那麼如何定義可選的參數呢?

與接口中的可選屬性類似,我們用 ? 表示可選的參數:

function buildName(firstName: string, lastName?: string) {
    if (lastName) {
        return firstName + ' ' + lastName;
    } else {
        return firstName;
    }
}
let tomcat = buildName('Tom', 'Cat');
let tom = buildName('Tom');

需要注意的是,可選參數必須接在必需參數後面。換句話說,可選參數後面不允許再出現必需參數了:

function buildName(firstName?: string, lastName: string) {
    if (firstName) {
        return firstName + ' ' + lastName;
    } else {
        return lastName;
    }
}
let tomcat = buildName('Tom', 'Cat');
let tom = buildName(undefined, 'Tom');

// index.ts(1,40): error TS1016: A required parameter cannot follow an optional parameter.

8.5、參數默認值

在 ES6 中,我們允許給函數的參數添加默認值,TypeScript 會將添加了默認值的參數識別爲可選參數:

function buildName(firstName: string, lastName: string = 'Cat') {
    return firstName + ' ' + lastName;
}
let tomcat = buildName('Tom', 'Cat');
let tom = buildName('Tom');

此時就不受「可選參數必須接在必需參數後面」的限制了:

function buildName(firstName: string = 'Tom', lastName: string) {
    return firstName + ' ' + lastName;
}
let tomcat = buildName('Tom', 'Cat');
let cat = buildName(undefined, 'Cat');

關於默認參數,可以參考 ES6 中函數參數的默認值

8.6、剩餘參數

ES6 中,可以使用 ...rest 的方式獲取函數中的剩餘參數(rest 參數):

function push(array, ...items) {
    items.forEach(function(item) {
        array.push(item);
    });
}

let a: any[] = [];
push(a, 1, 2, 3);

事實上,items 是一個數組。所以我們可以用數組的類型來定義它:

function push(array: any[], ...items: any[]) {
    items.forEach(function(item) {
        array.push(item);
    });
}

let a = [];
push(a, 1, 2, 3);

注意,rest 參數只能是最後一個參數,關於 rest 參數,可以參考 ES6 中的 rest 參數

8.7、重載

重載允許一個函數接受不同數量或類型的參數時,作出不同的處理。

比如,我們需要實現一個函數 reverse,輸入數字 123 的時候,輸出反轉的數字 321,輸入字符串 'hello' 的時候,輸出反轉的字符串 'olleh'

利用聯合類型,我們可以這麼實現:

function reverse(x: number | string): number | string | void {
    if (typeof x === 'number') {
        return Number(x.toString().split('').reverse().join(''));
    } else if (typeof x === 'string') {
        return x.split('').reverse().join('');
    }
}

然而這樣有一個缺點,就是不能夠精確的表達,輸入爲數字的時候,輸出也應該爲數字,輸入爲字符串的時候,輸出也應該爲字符串。

這時,我們可以使用重載定義多個 reverse 的函數類型:

function reverse(x: number): number;
function reverse(x: string): string;
function reverse(x: number | string): number | string | void {
    if (typeof x === 'number') {
        return Number(x.toString().split('').reverse().join(''));
    } else if (typeof x === 'string') {
        return x.split('').reverse().join('');
    }
}

上例中,我們重複定義了多次函數 reverse,前幾次都是函數定義,最後一次是函數實現。在編輯器的代碼提示中,可以正確的看到前兩個提示。

注意,TypeScript 會優先從最前面的函數定義開始匹配,所以多個函數定義如果有包含關係,需要優先把精確的定義寫在前面。

 九、類型斷言

類型斷言(Type Assertion)可以用來手動指定一個值的類型。

9.1、語法

as 類型

<類型>值

在 tsx 語法(React 的 jsx 語法的 ts 版)中必須使用前者,即 值 as 類型

形如 <Foo> 的語法在 tsx 中表示的是一個 ReactNode,在 ts 中除了表示類型斷言之外,也可能是表示一個泛型。

故建議大家在使用類型斷言時,統一使用 值 as 類型 這樣的語法,本書中也會貫徹這一思想。

9.2、類型斷言的用途

類型斷言的常見用途有以下幾種:

9.2.1、將一個聯合類型斷言爲其中一個類型

之前提到過,當 TypeScript 不確定一個聯合類型的變量到底是哪個類型的時候,我們只能訪問此聯合類型的所有類型中共有的屬性或方法:

interface Cat {
    name: string;
    run(): void;
}
interface Fish {
    name: string;
    swim(): void;
}

function getName(animal: Cat | Fish) {
    return animal.name;
}

而有時候,我們確實需要在還不確定類型的時候就訪問其中一個類型特有的屬性或方法,比如:

interface Cat {
    name: string;
    run(): void;
}
interface Fish {
    name: string;
    swim(): void;
}

function isFish(animal: Cat | Fish) {
    if (typeof animal.swim === 'function') {
        return true;
    }
    return false;
}

// index.ts:11:23 - error TS2339: Property 'swim' does not exist on type 'Cat | Fish'.
//   Property 'swim' does not exist on type 'Cat'.

上面的例子中,獲取 animal.swim 的時候會報錯。

此時可以使用類型斷言,將 animal 斷言成 Fish

interface Cat {
    name: string;
    run(): void;
}
interface Fish {
    name: string;
    swim(): void;
}

function isFish(animal: Cat | Fish) {
    if (typeof (animal as Fish).swim === 'function') {
        return true;
    }
    return false;
}

這樣就可以解決訪問 animal.swim 時報錯的問題了。

需要注意的是,類型斷言只能夠「欺騙」TypeScript 編譯器,無法避免運行時的錯誤,反而濫用類型斷言可能會導致運行時錯誤:

interface Cat {
    name: string;
    run(): void;
}
interface Fish {
    name: string;
    swim(): void;
}

function swim(animal: Cat | Fish) {
    (animal as Fish).swim();
}

const tom: Cat = {
    name: 'Tom',
    run() { console.log('run') }
};
swim(tom);
// Uncaught TypeError: animal.swim is not a function`

上面的例子編譯時不會報錯,但在運行時會報錯:

Uncaught TypeError: animal.swim is not a function`

原因是 (animal as Fish).swim() 這段代碼隱藏了 animal 可能爲 Cat 的情況,將 animal 直接斷言爲 Fish 了,而 TypeScript 編譯器信任了我們的斷言,故在調用 swim() 時沒有編譯錯誤。

可是 swim 函數接受的參數是 Cat | Fish,一旦傳入的參數是 Cat 類型的變量,由於 Cat 上沒有 swim 方法,就會導致運行時錯誤了。

總之,使用類型斷言時一定要格外小心,儘量避免斷言後調用方法或引用深層屬性,以減少不必要的運行時錯誤。

9.2.2、將一個父類斷言爲更加具體的子類

當類之間有繼承關係時,類型斷言也是很常見的:

class ApiError extends Error {
    code: number = 0;
}
class HttpError extends Error {
    statusCode: number = 200;
}

function isApiError(error: Error) {
    if (typeof (error as ApiError).code === 'number') {
        return true;
    }
    return false;
}

上面的例子中,我們聲明瞭函數 isApiError,它用來判斷傳入的參數是不是 ApiError 類型,爲了實現這樣一個函數,它的參數的類型肯定得是比較抽象的父類 Error,這樣的話這個函數就能接受 Error 或它的子類作爲參數了。

但是由於父類 Error 中沒有 code 屬性,故直接獲取 error.code 會報錯,需要使用類型斷言獲取 (error as ApiError).code

大家可能會注意到,在這個例子中有一個更合適的方式來判斷是不是 ApiError,那就是使用 instanceof

class ApiError extends Error {
    code: number = 0;
}
class HttpError extends Error {
    statusCode: number = 200;
}

function isApiError(error: Error) {
    if (error instanceof ApiError) {
        return true;
    }
    return false;
}

上面的例子中,確實使用 instanceof 更加合適,因爲 ApiError 是一個 JavaScript 的類,能夠通過 instanceof 來判斷 error 是否是它的實例。

但是有的情況下 ApiError 和 HttpError 不是一個真正的類,而只是一個 TypeScript 的接口(interface),接口是一個類型,不是一個真正的值,它在編譯結果中會被刪除,當然就無法使用 instanceof 來做運行時判斷了:

interface ApiError extends Error {
    code: number;
}
interface HttpError extends Error {
    statusCode: number;
}

function isApiError(error: Error) {
    if (error instanceof ApiError) {
        return true;
    }
    return false;
}

// index.ts:9:26 - error TS2693: 'ApiError' only refers to a type, but is being used as a value here.

此時就只能用類型斷言,通過判斷是否存在 code 屬性,來判斷傳入的參數是不是 ApiError 了:

interface ApiError extends Error {
    code: number;
}
interface HttpError extends Error {
    statusCode: number;
}

function isApiError(error: Error) {
    if (typeof (error as ApiError).code === 'number') {
        return true;
    }
    return false;
}

9.2.3、將任何一個類型斷言爲 any

理想情況下,TypeScript 的類型系統運轉良好,每個值的類型都具體而精確。

當我們引用一個在此類型上不存在的屬性或方法時,就會報錯:

const foo: number = 1;
foo.length = 1;

// index.ts:2:5 - error TS2339: Property 'length' does not exist on type 'number'.

上面的例子中,數字類型的變量 foo 上是沒有 length 屬性的,故 TypeScript 給出了相應的錯誤提示。

這種錯誤提示顯然是非常有用的。

但有的時候,我們非常確定這段代碼不會出錯,比如下面這個例子:

window.foo = 1;

// index.ts:1:8 - error TS2339: Property 'foo' does not exist on type 'Window & typeof globalThis'.

上面的例子中,我們需要將 window 上添加一個屬性 foo,但 TypeScript 編譯時會報錯,提示我們 window 上不存在 foo 屬性。

此時我們可以使用 as any 臨時將 window 斷言爲 any 類型:

(window as any).foo = 1;

在 any 類型的變量上,訪問任何屬性都是允許的。

需要注意的是,將一個變量斷言爲 any 可以說是解決 TypeScript 中類型問題的最後一個手段。

它極有可能掩蓋了真正的類型錯誤,所以如果不是非常確定,就不要使用 as any

上面的例子中,我們也可以通過[擴展 window 的類型(TODO)][]解決這個錯誤,不過如果只是臨時的增加 foo 屬性,as any 會更加方便。

總之,一方面不能濫用 as any,另一方面也不要完全否定它的作用,我們需要在類型的嚴格性和開發的便利性之間掌握平衡(這也是 TypeScript 的設計理念之一),才能發揮出 TypeScript 最大的價值。

9.2.4、將 any 斷言爲一個具體的類型

在日常的開發中,我們不可避免的需要處理 any 類型的變量,它們可能是由於第三方庫未能定義好自己的類型,也有可能是歷史遺留的或其他人編寫的爛代碼,還可能是受到 TypeScript 類型系統的限制而無法精確定義類型的場景。

遇到 any 類型的變量時,我們可以選擇無視它,任由它滋生更多的 any

我們也可以選擇改進它,通過類型斷言及時的把 any 斷言爲精確的類型,亡羊補牢,使我們的代碼向着高可維護性的目標發展。

舉例來說,歷史遺留的代碼中有個 getCacheData,它的返回值是 any

function getCacheData(key: string): any {
    return (window as any).cache[key];
}

那麼我們在使用它時,最好能夠將調用了它之後的返回值斷言成一個精確的類型,這樣就方便了後續的操作:

function getCacheData(key: string): any {
    return (window as any).cache[key];
}

interface Cat {
    name: string;
    run(): void;
}

const tom = getCacheData('tom') as Cat;
tom.run();

上面的例子中,我們調用完 getCacheData 之後,立即將它斷言爲 Cat 類型。這樣的話明確了 tom 的類型,後續對 tom 的訪問時就有了代碼補全,提高了代碼的可維護性。

9.3、類型斷言的限制

本小節的前置知識點:[結構類型系統(TODO)][]、[類型兼容性(TODO)][]

從上面的例子中,我們可以總結出:

  • 聯合類型可以被斷言爲其中一個類型
  • 父類可以被斷言爲子類
  • 任何類型都可以被斷言爲 any
  • any 可以被斷言爲任何類型

那麼類型斷言有沒有什麼限制呢?是不是任何一個類型都可以被斷言爲任何另一個類型呢?

答案是否定的——並不是任何一個類型都可以被斷言爲任何另一個類型。

具體來說,若 A 兼容 B,那麼 A 能夠被斷言爲 BB 也能被斷言爲 A

下面我們通過一個簡化的例子,來理解類型斷言的限制:

interface Animal {
    name: string;
}
interface Cat {
    name: string;
    run(): void;
}

let tom: Cat = {
    name: 'Tom',
    run: () => { console.log('run') }
};
let animal: Animal = tom;

我們知道,TypeScript 是結構類型系統,類型之間的對比只會比較它們最終的結構,而會忽略它們定義時的關係。

在上面的例子中,Cat 包含了 Animal 中的所有屬性,除此之外,它還有一個額外的方法 run。TypeScript 並不關心 Cat 和 Animal 之間定義時是什麼關係,而只會看它們最終的結構有什麼關係——所以它與 Cat extends Animal 是等價的:

interface Animal {
    name: string;
}
interface Cat extends Animal {
    run(): void;
}

那麼也不難理解爲什麼 Cat 類型的 tom 可以賦值給 Animal 類型的 animal 了——就像面向對象編程中我們可以將子類的實例賦值給類型爲父類的變量。

我們把它換成 TypeScript 中更專業的說法,即:Animal 兼容 Cat

當 Animal 兼容 Cat 時,它們就可以互相進行類型斷言了:

interface Animal {
    name: string;
}
interface Cat {
    name: string;
    run(): void;
}

function testAnimal(animal: Animal) {
    return (animal as Cat);
}
function testCat(cat: Cat) {
    return (cat as Animal);
}

這樣的設計其實也很容易就能理解:

  • 允許 animal as Cat 是因爲「父類可以被斷言爲子類」,這個前面已經學習過了
  • 允許 cat as Animal 是因爲既然子類擁有父類的屬性和方法,那麼被斷言爲父類,獲取父類的屬性、調用父類的方法,就不會有任何問題,故「子類可以被斷言爲父類」

需要注意的是,這裏我們使用了簡化的父類子類的關係來表達類型的兼容性,而實際上 TypeScript 在判斷類型的兼容性時,比這種情況複雜很多,詳細請參考[類型的兼容性(TODO)][]章節。

總之,若 A 兼容 B,那麼 A 能夠被斷言爲 BB 也能被斷言爲 A

同理,若 B 兼容 A,那麼 A 能夠被斷言爲 BB 也能被斷言爲 A

所以這也可以換一種說法:

要使得 A 能夠被斷言爲 B,只需要 A 兼容 B 或 B 兼容 A 即可,這也是爲了在類型斷言時的安全考慮,畢竟毫無根據的斷言是非常危險的。

綜上所述:

  • 聯合類型可以被斷言爲其中一個類型
  • 父類可以被斷言爲子類
  • 任何類型都可以被斷言爲 any
  • any 可以被斷言爲任何類型
  • 要使得 A 能夠被斷言爲 B,只需要 A 兼容 B 或 B 兼容 A 即可

其實前四種情況都是最後一個的特例。

9.4、雙重斷言

既然:

  • 任何類型都可以被斷言爲 any
  • any 可以被斷言爲任何類型

那麼我們是不是可以使用雙重斷言 as any as Foo 來將任何一個類型斷言爲任何另一個類型呢?

interface Cat {
    run(): void;
}
interface Fish {
    swim(): void;
}

function testCat(cat: Cat) {
    return (cat as any as Fish);
}

在上面的例子中,若直接使用 cat as Fish 肯定會報錯,因爲 Cat 和 Fish 互相都不兼容。

但是若使用雙重斷言,則可以打破「要使得 A 能夠被斷言爲 B,只需要 A 兼容 B 或 B 兼容 A 即可」的限制,將任何一個類型斷言爲任何另一個類型。

若你使用了這種雙重斷言,那麼十有八九是非常錯誤的,它很可能會導致運行時錯誤。

除非迫不得已,千萬別用雙重斷言。

9.5、類型斷言 vs 類型轉換

類型斷言只會影響 TypeScript 編譯時的類型,類型斷言語句在編譯結果中會被刪除:

function toBoolean(something: any): boolean {
    return something as boolean;
}

toBoolean(1);
// 返回值爲 1

在上面的例子中,將 something 斷言爲 boolean 雖然可以通過編譯,但是並沒有什麼用,代碼在編譯後會變成:

function toBoolean(something) {
    return something;
}

toBoolean(1);
// 返回值爲 1

所以類型斷言不是類型轉換,它不會真的影響到變量的類型。

若要進行類型轉換,需要直接調用類型轉換的方法:

function toBoolean(something: any): boolean {
    return Boolean(something);
}

toBoolean(1);
// 返回值爲 true

9.6、類型斷言 vs 類型聲明

在這個例子中:

function getCacheData(key: string): any {
    return (window as any).cache[key];
}

interface Cat {
    name: string;
    run(): void;
}

const tom = getCacheData('tom') as Cat;
tom.run();

我們使用 as Cat 將 any 類型斷言爲了 Cat 類型。

但實際上還有其他方式可以解決這個問題:

function getCacheData(key: string): any {
    return (window as any).cache[key];
}

interface Cat {
    name: string;
    run(): void;
}

const tom: Cat = getCacheData('tom');
tom.run();

上面的例子中,我們通過類型聲明的方式,將 tom 聲明爲 Cat,然後再將 any 類型的 getCacheData('tom') 賦值給 Cat 類型的 tom

這和類型斷言是非常相似的,而且產生的結果也幾乎是一樣的——tom 在接下來的代碼中都變成了 Cat 類型。

它們的區別,可以通過這個例子來理解:

interface Animal {
    name: string;
}
interface Cat {
    name: string;
    run(): void;
}

const animal: Animal = {
    name: 'tom'
};
let tom = animal as Cat;

在上面的例子中,由於 Animal 兼容 Cat,故可以將 animal 斷言爲 Cat 賦值給 tom

但是若直接聲明 tom 爲 Cat 類型:

interface Animal {
    name: string;
}
interface Cat {
    name: string;
    run(): void;
}

const animal: Animal = {
    name: 'tom'
};
let tom: Cat = animal;

// index.ts:12:5 - error TS2741: Property 'run' is missing in type 'Animal' but required in type 'Cat'.

則會報錯,不允許將 animal 賦值爲 Cat 類型的 tom

這很容易理解,Animal 可以看作是 Cat 的父類,當然不能將父類的實例賦值給類型爲子類的變量。

深入的講,它們的核心區別就在於:

  • animal 斷言爲 Cat,只需要滿足 Animal 兼容 Cat 或 Cat 兼容 Animal 即可
  • animal 賦值給 tom,需要滿足 Cat 兼容 Animal 纔行

但是 Cat 並不兼容 Animal

而在前一個例子中,由於 getCacheData('tom') 是 any 類型,any 兼容 CatCat 也兼容 any,故

const tom = getCacheData('tom') as Cat;

等價於

const tom: Cat = getCacheData('tom');

知道了它們的核心區別,就知道了類型聲明是比類型斷言更加嚴格的。

所以爲了增加代碼的質量,我們最好優先使用類型聲明,這也比類型斷言的 as 語法更加優雅。

9.7、類型斷言 vs 泛型

本小節的前置知識點:泛型

還是這個例子:

function getCacheData(key: string): any {
    return (window as any).cache[key];
}

interface Cat {
    name: string;
    run(): void;
}

const tom = getCacheData('tom') as Cat;
tom.run();

我們還有第三種方式可以解決這個問題,那就是泛型:

function getCacheData<T>(key: string): T {
    return (window as any).cache[key];
}

interface Cat {
    name: string;
    run(): void;
}

const tom = getCacheData<Cat>('tom');
tom.run();

通過給 getCacheData 函數添加了一個泛型 <T>,我們可以更加規範的實現對 getCacheData 返回值的約束,這也同時去除掉了代碼中的 any,是最優的一個解決方案。

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