Typescript最強入門

1. typescript介紹

1.1. 什么是typescript?

TypeScript簡稱TS
TS和JS之間的關系其實就是Less/Sass和CSS之間的關系
就像Less/Sass是對CSS進行擴展一樣, TS也是對JS進行擴展
就像Less/Sass最終會轉換成CSS一樣, 我們編寫好的TS代碼最終也會換成JS
TypeScript是JavaScript的超集,因為它擴展了JavaScript,有JavaScript沒有的東西
硬要以父子類關系來說的話,TypeScript是JavaScript子類,繼承的基礎上去擴展。

1.2. 為什么需要TypeScript?

簡單來說就是因為JavaScript是弱類型, 很多錯誤只有在運行時才會被發現
而TypeScript提供了一套靜態檢測機制, 可以幫助我們在編譯時就發現錯誤

1.3. TypeScript特點

支持最新的JavaScript新特性
支持代碼靜態檢查
支持諸如C,C++,Java,Go等后端語言中的特性 (枚舉、泛型、類型轉換、命名空間、聲明文件、類、接口等)

2. 搭建typescript學習環境

2.1. 安裝最新版typescript

npm i -g typescript

2.2. 安裝ts-node

npm i -g ts-node

2.3. 創建一個 tsconfig.json 文件

tsc --init

然后新建index.ts,輸入相關練習代碼,然后執行 ts-node index.ts

2.4. 官方playground

官方也提供了一個在線開發 TypeScript 的云環境——Playground
基于它,我們無須在本地安裝環境,只需要一個瀏覽器即可隨時學習和編寫 TypeScript,同時還可以方便地選擇 TypeScript 版本、配置 tsconfig,并對 TypeScript 實時靜態類型檢測、轉譯輸出 JavaScript 和在線執行。
而且在體驗上,它也一點兒不遜色于任何本地的 IDE,對于剛剛學習 TypeScript 的我們來說,算是一個不錯的選擇。

3. 基礎數據類型

3.1. JS的八種內置類型

let str: string = "tom";
let num: number = 24;
let bool: boolean = false;
let u: undefined = undefined;
let n: null = null;
let obj: object = {x: 1};
let big: bigint = 100n;
let sym: symbol = Symbol("me"); 

3.2. 注意點

null和undefined
默認情況下 null 和 undefined 是所有類型的子類型。
就是說你可以把 null 和 undefined 賦值給其他類型。

// null和undefined賦值給string
let str:string = "666";
str = null
str= undefined

// null和undefined賦值給number
let num:number = 666;
num = null
num= undefined

// null和undefined賦值給object
let obj:object ={};
obj = null
obj= undefined

// null和undefined賦值給Symbol
let sym: symbol = Symbol("me"); 
sym = null
sym= undefined

// null和undefined賦值給boolean
let isDone: boolean = false;
isDone = null
isDone= undefined

// null和undefined賦值給bigint
let big: bigint =  100n;
big = null
big= undefined

如果你在tsconfig.json指定了"strictNullChecks":true ,null 和 undefined 只能賦值給 void 和它們各自的類型。

number和bigint
雖然number和bigint都表示數字,但是這兩個類型不兼容。

let big: bigint =  100n;
let num: number = 6;
big = num;
num = big;

會拋出一個類型不兼容的 ts(2322) 錯誤。

3.3. 其他類型

Array
對數組類型的定義有兩種方式:

let arr:string[] = ["1","2"];
let arr2:Array<string> = ["1","2"];

定義聯合類型數組

let arr:(number | string)[];
// 表示定義了一個名稱叫做arr的數組,
// 這個數組中將來既可以存儲數值類型的數據, 也可以存儲字符串類型的數據
arr3 = [1, 'b', 2, 'c'];

定義指定對象成員的數組

// interface是接口,后面會講到
interface Arrobj{   
    name:string, 
    age:number
}
let arr3:Arrobj[]=[{name:'jimmy',age:22}]

函數
函數聲明

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

函數表達式

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

用接口定義函數類型

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

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

可選參數

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 = 'Cat') {   
    return firstName + ' ' + lastName;
}
let tomcat = buildName('Tom', 'Cat');
let tom = buildName('Tom');

剩余參數

function push(array: any[], ...items: any[]) {   
    items.forEach(function(item) {     
        array.push(item);  
    });
}
let a = [];
push(a, 1, 2, 3);

函數重載
由于 JavaScript 是一個動態語言,我們通常會使用不同類型的參數來調用同一個函數,該函數會根據不同的參數而返回不同的類型的調用結果:

function add(x, y) { 
 return x + y;
}
add(1, 2); // 3
add("1", "2"); //"12"

由于 TypeScript 是 JavaScript 的超集,因此以上的代碼可以直接在 TypeScript 中使用,但當 TypeScript 編譯器開啟 noImplicitAny 的配置項時,以上代碼會提示以下錯誤信息:

Parameter 'x' implicitly has an 'any' type.
Parameter 'y' implicitly has an 'any' type.

該信息告訴我們參數 x 和參數 y 隱式具有 any 類型。為了解決這個問題,我們可以為參數設置一個類型。因為我們希望 add 函數同時支持 string 和 number 類型,因此我們可以定義一個 string | number 聯合類型,同時我們為該聯合類型取個別名:

type Combinable = string | number;

在定義完 Combinable 聯合類型后,我們來更新一下 add 函數:

function add(a: Combinable, b: Combinable) {  
    if (typeof a === 'string' || typeof b === 'string') {    
     return a.toString() + b.toString();   
    }  
    return a + b;
}

為 add 函數的參數顯式設置類型之后,之前錯誤的提示消息就消失了。那么此時的 add 函數就完美了么,我們來實際測試一下:

const result = add('Semlinker', ' Kakuqo');
result.split(' ');

在上面代碼中,我們分別使用 'Semlinker' 和 ' Kakuqo' 這兩個字符串作為參數調用 add 函數,并把調用結果保存到一個名為 result 的變量上,這時候我們想當然的認為此時 result 的變量的類型為 string,所以我們就可以正常調用字符串對象上的 split 方法。但這時 TypeScript 編譯器又出現以下錯誤信息了:

Property 'split' does not exist on type 'number'.

很明顯 number 類型的對象上并不存在 split 屬性。問題又來了,那如何解決呢?這時我們就可以利用 TypeScript 提供的函數重載特性。

函數重載或方法重載是使用相同名稱和不同參數數量或類型創建多個方法的一種能力。 要解決前面遇到的問題,方法就是為同一個函數提供多個函數類型定義來進行函數重載,編譯器會根據這個列表去處理函數的調用。

type Types = number | string
function add(a:number,b:number):number;
function add(a: string, b: string): string;
function add(a: string, b: number): string;
function add(a: number, b: string): string;
function add(a:Types, b:Types) {  
  if (typeof a === 'string' || typeof b === 'string') {  
    return a.toString() + b.toString(); 
  }  
  return a + b;
}
const result = add('Semlinker', ' Kakuqo');
result.split(' ');

在以上代碼中,我們為 add 函數提供了多個函數類型定義,從而實現函數的重載。之后,可惡的錯誤消息又消失了,因為這時 result 變量的類型是 string 類型。

Tuple(元組)
元祖定義
眾所周知,數組一般由同種類型的值組成,但有時我們需要在單個變量中存儲不同類型的值,這時候我們就可以使用元組。在 JavaScript 中是沒有元組的,元組是 TypeScript 中特有的類型,其工作方式類似于數組。
元組最重要的特性是可以限制 數組元素的個數和類型,它特別適合用來實現多值返回。
元祖用于保存特定長度特點數據類型的數據

let x = [string,number]
// 類型必須匹配且個數必須為2
x = ['hello', 10]; // OK 
x = ['hello', 10,10]; // Error
x = [10, 'hello']; // Error

注意,元組類型只能表示一個已知元素數量和類型的數組,長度已指定,越界訪問會提示錯誤。
如果一個數組中可能有多種類型,數量和類型都不確定,那就直接any[]

元祖類型的解構賦值
我們可以通過下標的方式來訪問元組中的元素,當元組中的元素較多時,這種方式并不是那么便捷。其實元組也是支持解構賦值的:

let employee: [number, string] = [1, "Semlinker"];
let [id, username] = employee;
console.log(`id: ${id}`);
console.log(`username: ${username}`);

以上代碼成功運行后,控制臺會輸出以下消息:

id: 1
username: Semlinker

這里需要注意的是,在解構賦值時,解構數組元素的個數是不能超過元組中元素的個數,否則也會出現錯誤,比如:

let employee: [number, string] = [1, "Semlinker"];
let [id, username, age] = employee;

在以上代碼中,我們新增了一個 age 變量,但此時 TypeScript 編譯器會提示以下錯誤信息:

Tuple type '[number, string]' of length '2' has no element at index '2'.

很明顯元組類型 [number, string] 的長度是 2,在位置索引 2 處不存在任何元素。

元組類型的可選元素
與函數簽名類似,在定義元組類型時,我們也可以通過 ? 號來聲明元組類型的可選元素,具體的示例如下:

let optionalTuple: [string, boolean?];
optionalTuple = ["Semlinker", true];
console.log(`optionalTuple : ${optionalTuple}`);
optionalTuple = ["Kakuqo"];
console.log(`optionalTuple : ${optionalTuple}`);

在上面代碼中,我們定義了一個名為 optionalTuple 的變量,該變量的類型要求包含一個必須的字符串屬性和一個可選布爾屬性,該代碼正常運行后,控制臺會輸出以下內容:

optionalTuple : Semlinker,true
optionalTuple : Kakuqo

那么在實際工作中,聲明可選的元組元素有什么作用?這里我們來舉一個例子,在三維坐標軸中,一個坐標點可以使用 (x, y, z) 的形式來表示,對于二維坐標軸來說,坐標點可以使用 (x, y) 的形式來表示,而對于一維坐標軸來說,只要使用 (x) 的形式來表示即可。
針對這種情形,在 TypeScript 中就可以利用元組類型可選元素的特性來定義一個元組類型的坐標點,具體實現如下:

type Point = [number, number?, number?];

const x: Point = [10]; // 一維坐標點
const xy: Point = [10, 20]; // 二維坐標點
const xyz: Point = [10, 20, 10]; // 三維坐標點

console.log(x.length); // 1
console.log(xy.length); // 2
console.log(xyz.length); // 3

元組類型的剩余元素
元組類型里最后一個元素可以是剩余元素,形式為 ...X,這里 X 是數組類型。
剩余元素代表元組類型是開放的,可以有零個或多個額外的元素。
例如,[number, ...string[]] 表示帶有一個 number 元素和任意數量string 類型元素的元組類型。
為了能更好的理解,我們來舉個具體的例子:

type RestTupleType = [number, ...string[]];
let restTuple: RestTupleType = [666, "Semlinker", "Kakuqo", "Lolo"];
console.log(restTuple[0]);
console.log(restTuple[1]);

只讀的元組類型
TypeScript 3.4 還引入了對只讀元組的新支持。我們可以為任何元組類型加上 readonly 關鍵字前綴,以使其成為只讀元組。具體的示例如下:

const point: readonly [number, number] = [10, 20];

在使用 readonly 關鍵字修飾元組類型之后,任何企圖修改元組中元素的操作都會拋出異常:

// Cannot assign to '0' because it is a read-only property.
point[0] = 1;
// Property 'push' does not exist on type 'readonly [number, number]'.point.push(0);
// Property 'pop' does not exist on type 'readonly [number, number]'.point.pop();
// Property 'splice' does not exist on type 'readonly [number, number]'.point.splice(1, 1);

void
void表示沒有任何類型,和其他類型是平等關系,不能直接賦值:

let a: void;
let b: number = a; // Error

你只能為它賦予null和undefined(在strictNullChecks未指定為true時)。
聲明一個void類型的變量沒有什么大用,我們一般也只有在函數沒有返回值時去聲明。
值得注意的是,方法沒有返回值將得到undefined,但是我們需要定義成void類型,而不是undefined類型。否則將報錯:

function fun(): undefined {  
  console.log("this is TypeScript");
};
fun(); // Error

never
never類型表示的是那些永不存在的值的類型。
值會永不存在的兩種情況:

  1. 如果一個函數執行時拋出了異常,那么這個函數永遠不存在返回值(因為拋出異常會直接中斷程序運行,這使得程序運行不到返回值那一步,即具有不可達的終點,也就永不存在返回了)
  2. 函數中執行無限循環的代碼(死循環),使得程序永遠無法運行到函數返回值那一步,永不存在返回
// 異常
function err(msg: string): never { // OK 
  throw new Error(msg); 
}

// 死循環
function loopForever(): never { // OK  
  while (true) {};
}

never類型同null和undefined一樣,也是任何類型的子類型,也可以賦值給任何類型。
但是沒有類型是never的子類型或可以賦值給never類型(除了never本身之外),即使any也不可以賦值給never。

let ne: never;
let nev: never;
let an: any;

ne = 123; // Error
ne = nev; // OK
ne = an; // Error
ne = (() => { throw new Error("異常"); })(); // OK
ne = (() => { while(true) {} })(); // OK

在 TypeScript 中,可以利用 never 類型的特性來實現全面性檢查,具體示例如下:

type Foo = string | number;

function controlFlowAnalysisWithNever(foo: Foo) {
  if (typeof foo === "string") {  
    // 這里 foo 被收窄為 string 類型 
  } else if (typeof foo === "number") { 
    // 這里 foo 被收窄為 number 類型 
  } else {   
    // foo 在這里是 never  
    const check: never = foo; 
  }
}

注意在 else 分支里面,我們把收窄為 never 的 foo 賦值給一個顯示聲明的 never 變量。
如果一切邏輯正確,那么這里應該能夠編譯通過。
但是假如后來有一天你的同事修改了 Foo 的類型:

type Foo = string | number | boolean;

然而他忘記同時修改 controlFlowAnalysisWithNever 方法中的控制流程,這時候 else 分支的 foo 類型會被收窄為 boolean 類型,導致無法賦值給 never 類型,這時就會產生一個編譯錯誤。
通過這個方式,我們可以確保controlFlowAnalysisWithNever 方法總是窮盡了 Foo 的所有可能類型。
通過這個示例,我們可以得出一個結論:使用 never 避免出現新增了聯合類型沒有對應的實現,目的就是寫出類型絕對安全的代碼。

any
在 TypeScript 中,任何類型都可以被歸為 any 類型。這讓 any 類型成為了類型系統的頂級類型。
如果是一個普通類型,在賦值過程中改變類型是不被允許的:

let a: string = 'seven';
a = 7;
// TS2322: Type 'number' is not assignable to type 'string'.

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

let a: any = 666;
a = "Semlinker";
a = false;
a = 66
a = undefined
a = null
a = []
a = {}

在any上訪問任何屬性都是允許的,也允許調用任何方法。

let anyThing: any = 'hello';
console.log(anyThing.myName);
console.log(anyThing.myName.firstName);
let anyThing: any = 'Tom';
anyThing.setName('Jerry');
anyThing.setName('Jerry').sayHello();
anyThing.myName.setFirstName('Cat');

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

let something;
something = 'seven';
something = 7;
something.setName('Tom');

等價于

let something: any;
something = 'seven';
something = 7;
something.setName('Tom');

在許多場景下,這太寬松了。使用 any 類型,可以很容易地編寫類型正確但在運行時有問題的代碼。
如果我們使用 any 類型,就無法使用 TypeScript 提供的大量的保護機制。請記住,any 是魔鬼! 盡量不要用any。
為了解決 any 帶來的問題,TypeScript 3.0 引入了 unknown 類型。

unknown
unknown與any一樣,所有類型都可以分配給unknown:

let notSure: unknown = 4;
notSure = "maybe a string instead"; // OK
notSure = false; // OK

unknown與any的最大區別是:任何類型的值可以賦值給any,同時any類型的值也可以賦值給任何類型。
unknown 任何類型的值都可以賦值給它,但它只能賦值給unknown和any

let notSure: unknown = 4;
let uncertain: any = notSure; // OK

let notSure: any = 4;
let uncertain: unknown = notSure; // OK

let notSure: unknown = 4;
let uncertain: number = notSure; // Error

如果不縮小類型,就無法對unknown類型執行任何操作:

function getDog() { 
 return '123'
} 

const dog: unknown = {hello: getDog};
dog.hello(); // Error

這種機制起到了很強的預防性,更安全,這就要求我們必須縮小類型,我們可以使用 typeof、類型斷言 等方式來縮小未知范圍:

function getDogName() { 
 let x: unknown; 
 return x;
};
const dogName = getDogName();
// 直接使用
const upName = dogName.toLowerCase(); // Error
// typeof
if (typeof dogName === 'string') { 
  const upName = dogName.toLowerCase(); // OK
}
// 類型斷言 
const upName = (dogName as string).toLowerCase(); // OK

Number、String、Boolean、Symbol
首先,我們來回顧一下初學 TypeScript 時,很容易和原始類型 number、string、boolean、symbol 混淆的首字母大寫的 Number、String、Boolean、Symbol 類型,后者是相應原始類型的包裝對象,姑且把它們稱之為對象類型
從類型兼容性上看,原始類型兼容對應的對象類型,反過來對象類型不兼容對應的原始類型。
下面我們看一個具體的示例:

let num: number;
let Num: Number;
Num = num; // ok
num = Num; // ts(2322)報錯

在示例中的第 3 行,我們可以把 number 賦給類型 Number,但在第 4 行把 Number 賦給 number 就會提示 ts(2322) 錯誤。
因此,我們需要銘記不要使用對象類型來注解值的類型,因為這沒有任何意義。

object、Object 和 {}
另外,object(首字母小寫,以下稱“小 object”)、Object(首字母大寫,以下稱“大 Object”)和 {}(以下稱“空對象”)
小 object 代表的是所有非原始類型,也就是說我們不能把 number、string、boolean、symbol等 原始類型賦值給 object。
在嚴格模式下,null 和 undefined 類型也不能賦給 object。

JavaScript 中以下類型被視為原始類型:string、boolean、number、bigint、symbol、null 和 undefined。
下面我們看一個具體示例:

let lowerCaseObject: object;
lowerCaseObject = 1; // ts(2322)
lowerCaseObject = 'a'; // ts(2322)
lowerCaseObject = true; // ts(2322)
lowerCaseObject = null; // ts(2322)
lowerCaseObject = undefined; // ts(2322)
lowerCaseObject = {}; // ok

在示例中的第 2~6 行都會提示 ts(2322) 錯誤,但是我們在第 7 行把一個空對象賦值給 object 后,則可以通過靜態類型檢測。
大Object 代表所有擁有 toString、hasOwnProperty 方法的類型,所以所有原始類型、非原始類型都可以賦給 Object。
同樣,在嚴格模式下,null 和 undefined 類型也不能賦給 Object。
下面我們也看一個具體的示例:

let upperCaseObject: Object;
upperCaseObject = 1; // ok
upperCaseObject = 'a'; // ok
upperCaseObject = true; // ok
upperCaseObject = null; // ts(2322)
upperCaseObject = undefined; // ts(2322)
upperCaseObject = {}; // ok

在示例中的第 2到4 行、第 7 行都可以通過靜態類型檢測,而第 5~6 行則會提示 ts(2322) 錯誤。
從上面示例可以看到,大 Object 包含原始類型,小 object 僅包含非原始類型,所以大 Object 似乎是小 object 的父類型。
實際上,大 Object 不僅是小 object 的父類型,同時也是小 object 的子類型。
下面我們還是通過一個具體的示例進行說明。

type isLowerCaseObjectExtendsUpperCaseObject = object extends Object ? true : false; // true
type isUpperCaseObjectExtendsLowerCaseObject = Object extends object ? true : false; // true
upperCaseObject = lowerCaseObject; // ok
lowerCaseObject = upperCaseObject; // ok

在示例中的第 1 行和第 2 行返回的類型都是 true,第3 行和第 4 行的 upperCaseObject 與 lowerCaseObject 可以互相賦值。

注意:盡管官方文檔說可以使用小 object 代替大 Object,但是我們仍要明白大 Object 并不完全等價于小 object。

{}空對象類型和大 Object 一樣,也是表示原始類型和非原始類型的集合,并且在嚴格模式下,null 和 undefined 也不能賦給 {} ,如下示例:

let ObjectLiteral: {};
ObjectLiteral = 1; // ok
ObjectLiteral = 'a'; // ok
ObjectLiteral = true; // ok
ObjectLiteral = null; // ts(2322)
ObjectLiteral = undefined; // ts(2322)
ObjectLiteral = {}; // ok
type isLiteralCaseObjectExtendsUpperCaseObject = {} extends Object ? true : false; // true
type isUpperCaseObjectExtendsLiteralCaseObject = Object extends {} ? true : false; // true
upperCaseObject = ObjectLiteral;
ObjectLiteral = upperCaseObject;

在示例中的第 8 行和第 9 行返回的類型都是 true,第10 行和第 11 行的 ObjectLiteral 與 upperCaseObject 可以互相賦值,第2~4 行、第 7 行的賦值操作都符合靜態類型檢測;而第5 行、第 6 行則會提示 ts(2322) 錯誤。
綜上結論:{}、大 Object 是比小 object 更寬泛的類型(least specific),{} 和大 Object 可以互相代替,用來表示原始類型(null、undefined 除外)和非原始類型;而小 object 則表示非原始類型。

4. 類型推斷

{ 
  let str: string = 'this is string';
  let num: number = 1; 
  let bool: boolean = true;
}
{
  const str: string = 'this is string';  
  const num: number = 1;
  const bool: boolean = true;
}

看著上面的示例,可能你已經在嘀咕了:定義基礎類型的變量都需要寫明類型注解,TypeScript 太麻煩了吧?在示例中,使用 let 定義變量時,我們寫明類型注解也就罷了,畢竟值可能會被改變。可是,使用 const 常量時還需要寫明類型注解,那可真的很麻煩。
實際上,TypeScript 早就考慮到了這么簡單而明顯的問題。
在很多情況下,TypeScript 會根據上下文環境自動推斷出變量的類型,無須我們再寫明類型注解。因此,上面的示例可以簡化為如下所示內容:

{
  let str = 'this is string'; // 等價 
  let num = 1; // 等價 
  let bool = true; // 等價
}
{ 
  const str = 'this is string'; // 不等價 
  const num = 1; // 不等價 
  const bool = true; // 不等價
}

我們把 TypeScript 這種基于賦值表達式推斷類型的能力稱之為類型推斷
在 TypeScript 中,具有初始化值的變量、有默認值的函數參數、函數返回的類型都可以根據上下文推斷出來。比如我們能根據 return 語句推斷函數返回的類型,如下代碼所示:

{
  /** 根據參數的類型,推斷出返回值的類型也是 number */ 
  function add1(a: number, b: number) {   
    return a + b;  
  } 
  const x1= add1(1, 1); // 推斷出 x1 的類型也是 number   
  
  /** 推斷參數 b 的類型是數字或者 undefined,返回值的類型也是數字 */ 
  function add2(a: number, b = 1) {  
    return a + b; 
  } 
  const x2 = add2(1); 
  // ts(2345) Argument of type "1" is not assignable to parameter of type 'number | undefined
  const x3 = add2(1, '1'); 
}

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

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

5. 類型斷言

有時候你會遇到這樣的情況,你會比 TypeScript 更了解某個值的詳細信息。通常這會發生在你清楚地知道一個實體具有比它現有類型更確切的類型。
通過類型斷言這種方式可以告訴編譯器,“相信我,我知道自己在干什么”。類型斷言好比其他語言里的類型轉換,但是不進行特殊的數據檢查和解構。它沒有運行時的影響,只是在編譯階段起作用。
TypeScript 類型檢測無法做到絕對智能,畢竟程序不能像人一樣思考。有時會碰到我們比 TypeScript 更清楚實際類型的情況,比如下面的例子:

const arrayNumber: number[] = [1, 2, 3, 4];
const greaterThan2: number = arrayNumber.find(num => num > 2); // 提示 ts(2322)

其中,greaterThan2 一定是一個數字(確切地講是 3),因為 arrayNumber 中明顯有大于 2 的成員,但靜態類型對運行時的邏輯無能為力。
在 TypeScript 看來,greaterThan2 的類型既可能是數字,也可能是 undefined,所以上面的示例中提示了一個 ts(2322) 錯誤,此時我們不能把類型 undefined 分配給類型 number。
不過,我們可以使用一種篤定的方式——類型斷言(類似僅作用在類型層面的強制類型轉換)告訴 TypeScript 按照我們的方式做類型檢查。
比如,我們可以使用 as 語法做類型斷言,如下代碼所示:

const arrayNumber: number[] = [1, 2, 3, 4];
const greaterThan2: number = arrayNumber.find(num => num > 2) as number;

語法

// 尖括號 語法
let someValue: any = "this is a string";
let strLength: number = (<string>someValue).length;

// as 語法
let someValue: any = "this is a string";
let strLength: number = (someValue as string).length;

以上兩種方式雖然沒有任何區別,但是尖括號格式會與react中JSX產生語法沖突,因此我們更推薦使用 as 語法。

非空斷言
在上下文中當類型檢查器無法斷定類型時,一個新的后綴表達式操作符 ! 可以用于斷言操作對象是非 null 和非 undefined 類型。
具體而言,x! 將從 x 值域中排除 null 和 undefined 。
具體看以下示例:

let mayNullOrUndefinedOrString: null | undefined | string;
mayNullOrUndefinedOrString!.toString(); // ok
mayNullOrUndefinedOrString.toString(); // ts(2531)
type NumGenerator = () => number;

function myFunc(numGenerator: NumGenerator | undefined) {  
  // Object is possibly 'undefined'.(2532) 
  // Cannot invoke an object which is possibly 'undefined'.(2722) 
  const num1 = numGenerator(); // Error 
  const num2 = numGenerator!(); //OK
}

確定賦值斷言
允許在實例屬性和變量聲明后面放置一個 ! 號,從而告訴 TypeScript 該屬性會被明確地賦值。為了更好地理解它的作用,我們來看個具體的例子:

let x: number;
initialize();

// Variable 'x' is used before being assigned.(2454)
console.log(2 * x); // Error
function initialize() { 
  x = 10;
}

很明顯該異常信息是說變量 x 在賦值前被使用了,要解決該問題,我們可以使用確定賦值斷言:

let x!: number;
initialize();
console.log(2 * x); // Ok

function initialize() { 
  x = 10;
}

通過 let x!: number; 確定賦值斷言,TypeScript 編譯器就會知道該屬性會被明確地賦值。

5. 字面量類型

在 TypeScript 中,字面量不僅可以表示值,還可以表示類型,即所謂的字面量類型。
目前,TypeScript 支持 3 種字面量類型:字符串字面量類型、數字字面量類型、布爾字面量類型,對應的字符串字面量、數字字面量、布爾字面量分別擁有與其值一樣的字面量類型,具體示例如下:

{ 
  let specifiedStr: 'this is string' = 'this is string'; 
  let specifiedNum: 1 = 1; 
  let specifiedBoolean: true = true;
}

比如 'this is string' (這里表示一個字符串字面量類型)類型是 string 類型(確切地說是 string 類型的子類型),而 string 類型不一定是 'this is string'(這里表示一個字符串字面量類型)類型,如下具體示例:

{ 
  let specifiedStr: 'this is string' = 'this is string'; 
  let str: string = 'any string'; 
  specifiedStr = str; // ts(2322) 類型 '"string"' 不能賦值給類型 'this is string'
  str = specifiedStr; // ok
}

比如說我們用“馬”比喻 string 類型,即“黑馬”代指 'this is string' 類型,“黑馬”肯定是“馬”,但“馬”不一定是“黑馬”,它可能還是“白馬”“灰馬”。因此,'this is string' 字面量類型可以給 string 類型賦值,但是 string 類型不能給 'this is string' 字面量類型賦值,這個比喻同樣適合于形容數字、布爾等其他字面量和它們父類的關系。

字符串字面量類型
一般來說,我們可以使用一個字符串字面量類型作為變量的類型,如下代碼所示:

let hello: 'hello' = 'hello';
hello = 'hi'; // ts(2322) Type '"hi"' is not assignable to type '"hello"'

實際上,定義單個的字面量類型并沒有太大的用處,它真正的應用場景是可以把多個字面量類型組合成一個聯合類型(后面會講解),用來描述擁有明確成員的實用的集合。
如下代碼所示,我們使用字面量聯合類型描述了一個明確、可 'up' 可 'down' 的集合,這樣就能清楚地知道需要的數據結構了。

type Direction = 'up' | 'down';

function move(dir: Direction) { 
  // ...
}
move('up'); // ok
move('right'); // ts(2345) Argument of type '"right"' is not assignable to parameter of type 'Direction'

通過使用字面量類型組合的聯合類型,我們可以限制函數的參數為指定的字面量類型集合,然后編譯器會檢查參數是否是指定的字面量類型集合里的成員。
因此,相較于使用 string 類型,使用字面量類型(組合的聯合類型)可以將函數的參數限定為更具體的類型。這不僅提升了程序的可讀性,還保證了函數的參數類型,可謂一舉兩得。

數字字面量類型及布爾字面量類型
數字字面量類型和布爾字面量類型的使用與字符串字面量類型的使用類似,我們可以使用字面量組合的聯合類型將函數的參數限定為更具體的類型,比如聲明如下所示的一個類型
Config:

interface Config {   
    size: 'small' | 'big';  
    isEnable:  true | false;   
    margin: 0 | 2 | 4;
}

在上述代碼中,我們限定了 size 屬性為字符串字面量類型 'small' | 'big',isEnable 屬性為布爾字面量類型 true | false(布爾字面量只包含 true 和 false,true | false 的組合跟直接使用 boolean 沒有區別),margin 屬性為數字字面量類型 0 | 2 | 4。

let和const分析
我們先來看一個 const 示例,如下代碼所示:

{  
  const str = 'this is string'; // str: 'this is string' 
  const num = 1; // num: 1 
  const bool = true; // bool: true
}

在上述代碼中,我們將 const 定義為一個不可變更的常量,在缺省類型注解的情況下,TypeScript 推斷出它的類型直接由賦值字面量的類型決定,這也是一種比較合理的設計。
接下來我們看看如下所示的 let 示例:

{ 
  let str = 'this is string'; // str: string 
  let num = 1; // num: number 
  let bool = true; // bool: boolean
}

在上述代碼中,缺省顯式類型注解的可變更的變量的類型轉換為了賦值字面量類型的父類型,比如 str 的類型是 'this is string' 類型(這里表示一個字符串字面量類型)的父類型 string,num 的類型是 1 類型的父類型 number。
這種設計符合編程預期,意味著我們可以分別賦予 str 和 num 任意值(只要類型是 string 和 number 的子集的變量):

str = 'any string'; 
num = 2; 
bool = false;

我們將 TypeScript 的字面量子類型轉換為父類型的這種設計稱之為 "literal widening",也就是字面量類型的拓寬,比如上面示例中提到的字符串字面量類型轉換成 string 類型,下面我們著重介紹一下。

6. 類型拓寬(Type Widening)

所有通過 let 或 var 定義的變量、函數的形參、對象的非只讀屬性,如果滿足指定了初始值且未顯式添加類型注解的條件,那么它們推斷出來的類型就是指定的初始值字面量類型拓寬后的類型,這就是字面量類型拓寬。
下面我們通過字符串字面量的示例來理解一下字面量類型拓寬:

let str = 'this is string'; // 類型是 string  
let strFun = (str = 'this is string') => str; // 類型是 (str?: string) => string; 
const specifiedStr = 'this is string'; // 類型是 'this is string' 
let str2 = specifiedStr; // 類型是 'string' 
let strFun2 = (str = specifiedStr) => str; // 類型是 (str?: string) => string;

因為第 1~2 行滿足了 let、形參且未顯式聲明類型注解的條件,所以變量、形參的類型拓寬為 string(形參類型確切地講是 string | undefined)。
因為第 3 行的常量不可變更,類型沒有拓寬,所以 specifiedStr 的類型是 'this is string' 字面量類型。
第 4~5 行,因為賦予的值 specifiedStr 的類型是字面量類型,且沒有顯式類型注解,所以變量、形參的類型也被拓寬了。其實,這樣的設計符合實際編程訴求。
基于字面量類型拓寬的條件,我們可以通過如下所示代碼添加顯示類型注解控制類型拓寬行為。

{  
  const specifiedStr: 'this is string' = 'this is string'; // 類型是 '"this is string"' 
  let str2 = specifiedStr; // 即便使用 let 定義,類型是 'this is string'
}

實際上,除了字面量類型拓寬之外,TypeScript 對某些特定類型值也有類似 "Type Widening" (類型拓寬)的設計,下面我們具體來了解一下。
比如對 null 和 undefined 的類型進行拓寬,通過 let、var 定義的變量如果滿足未顯式聲明類型注解且被賦予了 null 或 undefined 值,則推斷出這些變量的類型是 any:

{ 
  let x = null; // 類型拓寬成 any  
  let y = undefined; // 類型拓寬成 any 
  
  /** -----分界線------- */ 
  const z = null; // 類型是 null 
  
  /** -----分界線------- */ 
  let anyFun = (param = null) => param; // 形參類型是 null 
  let z2 = z; // 類型是 null 
  let x2 = x; // 類型是 null 
  let y2 = y; // 類型是 undefined
}

注意:在嚴格模式下,一些比較老的版本中(2.0)null 和 undefined 并不會被拓寬成“any”。
為了更方便的理解類型拓寬,下面我們舉個例子,更加深入的分析一下
假設你正在編寫一個向量庫,你首先定義了一個 Vector3 接口,然后定義了 getComponent 函數用于獲取指定坐標軸的值:

interface Vector3 { 
  x: number; 
  y: number; 
  z: number;
}

function getComponent(vector: Vector3, axis: "x" | "y" | "z") {  
  return vector[axis];
}

但是,當你嘗試使用 getComponent 函數時,TypeScript 會提示以下錯誤信息:

let x = "x";
let vec = { x: 10, y: 20, z: 30 };
// 類型“string”的參數不能賦給類型“"x" | "y" | "z"”的參數。
getComponent(vec, x); // Error

為什么會出現上述錯誤呢?通過 TypeScript 的錯誤提示消息,我們知道是因為變量 x 的類型被推斷為 string 類型,而 getComponent 函數期望它的第二個參數有一個更具體的類型。這在實際場合中被拓寬了,所以導致了一個錯誤。
這個過程是復雜的,因為對于任何給定的值都有許多可能的類型。例如:

const arr = ['x', 1];

上述 arr 變量的類型應該是什么?這里有一些可能性:

  • ('x' | 1)[]
  • ['x', 1]
  • [string, number]
  • readonly [string, number]
  • (string | number)[]
  • readonly (string|number)[]
  • [any, any]
  • any[]

沒有更多的上下文,TypeScript 無法知道哪種類型是 “正確的”,它必須猜測你的意圖。盡管 TypeScript 很聰明,但它無法讀懂你的心思。它不能保證 100% 正確,正如我們剛才看到的那樣的疏忽性錯誤。

在下面的例子中,變量 x 的類型被推斷為字符串,因為 TypeScript 允許這樣的代碼:

let x = 'semlinker';
x = 'kakuqo';
x = 'lolo';

對于 JavaScript 來說,以下代碼也是合法的:

let x = 'x';
x = /x|y|z/;
x = ['x', 'y', 'z'];

在推斷 x 的類型為字符串時,TypeScript 試圖在特殊性和靈活性之間取得平衡。一般規則是,變量的類型在聲明之后不應該改變,因此 string 比 string|RegExp 或 string|string[] 或任何字符串更有意義。

TypeScript 提供了一些控制拓寬過程的方法。其中一種方法是使用 const。如果用 const 而不是 let 聲明一個變量,那么它的類型會更窄。事實上,使用 const 可以幫助我們修復前面例子中的錯誤:

const x = "x"; // type is "x" 
let vec = { x: 10, y: 20, z: 30 };
getComponent(vec, x); // OK

因為 x 不能重新賦值,所以 TypeScript 可以推斷更窄的類型,就不會在后續賦值中出現錯誤。因為字符串字面量型 “x” 可以賦值給 "x"|"y"|"z",所以代碼會通過類型檢查器的檢查。
然而,const 并不是萬靈藥。對于對象和數組,仍然會存在問題
以下這段代碼在 JavaScript 中是沒有問題的:

const obj = { 
  x: 1,
};

obj.x = 6;
obj.x = '6';

obj.y = 8;
obj.name = 'semlinker';

而在 TypeScript 中,對于 obj 的類型來說,它可以是 {readonly x:1} 類型,或者是更通用的 {x:number} 類型。當然也可能是 {[key: string]: number} 或 object 類型。對于對象,TypeScript 的拓寬算法會將其內部屬性視為將其賦值給 let 關鍵字聲明的變量,進而來推斷其屬性的類型。
因此 obj 的類型為 {x:number} 。這使得你可以將 obj.x 賦值給其他 number 類型的變量,而不是 string 類型的變量,并且它還會阻止你添加其他屬性。
因此最后三行的語句會出現錯誤:

const obj = {  
  x: 1,
};

obj.x = 6; // OK 

// Type '"6"' is not assignable to type 'number'.obj.x = '6'; // Error

// Property 'y' does not exist on type '{ x: number; }'.obj.y = 8; // Error

// Property 'name' does not exist on type '{ x: number; }'.obj.name = 'semlinker'; // Error

TypeScript 試圖在具體性和靈活性之間取得平衡。它需要推斷一個足夠具體的類型來捕獲錯誤,但又不能推斷出錯誤的類型。它通過屬性的初始化值來推斷屬性的類型,當然有幾種方法可以覆蓋 TypeScript 的默認行為。一種是提供顯式類型注釋:

// Type is { x: 1 | 3 | 5; }
const obj: { x: 1 | 3 | 5 } = {  
  x: 1
};

另一種方法是使用 const 斷言。不要將其與 let 和 const 混淆,后者在值空間中引入符號。這是一個純粹的類型級構造。讓我們來看看以下變量的不同推斷類型:

// Type is { x: number; y: number; }
const obj1 = {  
  x: 1,
  y: 2
};

// Type is { x: 1; y: number; }
const obj2 = {
  x: 1 as const, 
  y: 2,
};

// Type is { readonly x: 1; readonly y: 2; }
const obj3 = { 
  x: 1, 
  y: 2
} as const;

當你在一個值之后使用 const 斷言時,TypeScript 將為它推斷出最窄的類型,沒有拓寬。對于真正的常量,這通常是你想要的。當然你也可以對數組使用 const 斷言:

// Type is number[]
const arr1 = [1, 2, 3]; 

// Type is readonly [1, 2, 3]
const arr2 = [1, 2, 3] as const;

既然有類型拓寬,自然也會有類型縮小,下面我們簡單介紹一下 Type Narrowing。

7. 類型縮小(Type Narrowing)

在 TypeScript 中,我們可以通過某些操作將變量的類型由一個較為寬泛的集合縮小到相對較小、較明確的集合,這就是 "Type Narrowing"。
比如,我們可以使用類型守衛(后面會講到)將函數參數的類型從 any 縮小到明確的類型,
具體示例如下:

{  
  let func = (anything: any) => {  
    if (typeof anything === 'string') {  
      return anything; // 類型是 string   
    } else if (typeof anything === 'number') {  
      return anything; // 類型是 number  
    }  
    return null; 
  };
}

在 VS Code 中 hover 到第 4 行的 anything 變量提示類型是 string,到第 6 行則提示類型是 number。
同樣,我們可以使用類型守衛將聯合類型縮小到明確的子類型,具體示例如下:

{  
  let func = (anything: string | number) => {  
    if (typeof anything === 'string') {   
      return anything; // 類型是 string  
    } else {  
      return anything; // 類型是 number  
    } 
  };
}

當然,我們也可以通過字面量類型等值判斷(===)或其他控制流語句(包括但不限于 if、三目運算符、switch 分支)將聯合類型收斂為更具體的類型,如下代碼所示:

{ 
  type Goods = 'pen' | 'pencil' |'ruler'; 
  const getPenCost = (item: 'pen') => 2;  
  const getPencilCost = (item: 'pencil') => 4; 
  const getRulerCost = (item: 'ruler') => 6; 
  const getCost = (item: Goods) =>  {
    if (item === 'pen') {    
      return getPenCost(item); // item => 'pen'  
    } else if (item === 'pencil') {   
      return getPencilCost(item); // item => 'pencil' 
    } else {  
      return getRulerCost(item); // item => 'ruler'   
    } 
  }
}

在上述 getCost 函數中,接受的參數類型是字面量類型的聯合類型,函數內包含了 if 語句的 3 個流程分支,其中每個流程分支調用的函數的參數都是具體獨立的字面量類型。
那為什么類型由多個字面量組成的變量 item 可以傳值給僅接收單一特定字面量類型的函數 getPenCost、getPencilCost、getRulerCost 呢?這是因為在每個流程分支中,編譯器知道流程分支中的 item 類型是什么。比如 item === 'pencil' 的分支,item 的類型就被收縮為“pencil”。
事實上,如果我們將上面的示例去掉中間的流程分支,編譯器也可以推斷出收斂后的類型,如下代碼所示:

const getCost = (item: Goods) =>  {  
  if (item === 'pen') {   
      item; // item => 'pen' 
  } else {     
      item; // => 'pencil' | 'ruler'  
  }  
}

一般來說 TypeScript 非常擅長通過條件來判別類型,但在處理一些特殊值時要特別注意 —— 它可能包含你不想要的東西!例如,以下從聯合類型中排除 null 的方法是錯誤的:

const el = document.getElementById("foo"); // Type is HTMLElement | null
if (typeof el === "object") { 
  el; // Type is HTMLElement | null
}

因為在 JavaScript 中 typeof null 的結果是 "object" ,所以你實際上并沒有通過這種檢查排除 null 值。除此之外,falsy 的原始值也會產生類似的問題:

function foo(x?: number | string | null) { 
  if (!x) {  
    x; // Type is string | number | null | undefined\ 
  }
}

因為空字符串和 0 都屬于 falsy 值,所以在分支中 x 的類型可能是 string 或 number 類型。幫助類型檢查器縮小類型的另一種常見方法是在它們上放置一個明確的 “標簽”:

interface UploadEvent { 
  type: "upload";
  filename: string; 
  contents: string;
}

interface DownloadEvent {  
  type: "download"; 
  filename: string;
}

type AppEvent = UploadEvent | DownloadEvent;

function handleEvent(e: AppEvent) { 
  switch (e.type) {   
    case "download":  
      e; // Type is DownloadEvent    
      break;  
    case "upload":    
      e; // Type is UploadEvent   
      break;  
  }
}

這種模式也被稱為 ”標簽聯合“ 或 ”可辨識聯合“,它在 TypeScript 中的應用范圍非常廣。

8. 聯合類型

聯合類型表示取值可以為多種類型中的一種,使用 | 分隔每個類型。

let myFavoriteNumber: string | number;
myFavoriteNumber = 'seven'; // OK
myFavoriteNumber = 7; // OK

聯合類型通常與 null 或 undefined 一起使用:

const sayHello = (name: string | undefined) => {  
  /* ... */
};

例如,這里 name 的類型是 string | undefined 意味著可以將 string 或 undefined 的值傳遞給sayHello 函數。

sayHello("semlinker"); 
sayHello(undefined);

通過這個示例,你可以憑直覺知道類型 A 和類型 B 聯合后的類型是同時接受 A 和 B 值的類型。此外,對于聯合類型來說,你可能會遇到以下的用法:

let num: 1 | 2 = 1;
type EventNames = 'click' | 'scroll' | 'mousemove';

以上示例中的 1、2 或 'click' 被稱為字面量類型,用來約束取值只能是某幾個值中的一個。

9. 類型別名

類型別名用來給一個類型起個新名字。類型別名常用于聯合類型。

type Message = string | string[];
let greet = (message: Message) => { 
  // ...
};

注意:類型別名,誠如其名,即我們僅僅是給類型取了一個新的名字,并不是創建了一個新的類型。

10. 交叉類型

交叉類型是將多個類型合并為一個類型。這讓我們可以把現有的多種類型疊加到一起成為一種類型,它包含了所需的所有類型的特性,使用&定義交叉類型。

{ 
  type Useless = string & number;
}

很顯然,如果我們僅僅把原始類型、字面量類型、函數類型等原子類型合并成交叉類型,是沒有任何用處的,因為任何類型都不能滿足同時屬于多種原子類型,比如既是 string 類型又是 number 類型。因此,在上述的代碼中,類型別名 Useless 的類型就是個 never。
交叉類型真正的用武之地就是將多個接口類型合并成一個類型,從而實現等同接口繼承的效果,也就是所謂的合并接口類型,如下代碼所示:

type IntersectionType = { id: number; name: string; } & { age: number }; 
const mixed: IntersectionType = {  
    id: 1,   
    name: 'name',  
    age: 18
}

在上述示例中,我們通過交叉類型,使得 IntersectionType 同時擁有了 id、name、age 所有屬性,這里我們可以試著將合并接口類型理解為求并集。

思考

這里,我們來發散思考一下:如果合并的多個接口類型存在同名屬性會是什么效果呢?
如果同名屬性的類型不兼容,比如上面示例中兩個接口類型同名的 name 屬性類型一個是 number,另一個是 string,合并后,name 屬性的類型就是 number 和 string 兩個原子類型的交叉類型,即 never,如下代碼所示:

type IntersectionTypeConfict = { id: number; name: string; }  
  & { age: number; name: number; };  
  const mixedConflict: IntersectionTypeConfict = {  
    id: 1,  
    name: 2, // ts(2322) 錯誤,'number' 類型不能賦給 'never' 類型   
    age: 2  
  };

此時,我們賦予 mixedConflict 任意類型的 name 屬性值都會提示類型錯誤。而如果我們不設置 name 屬性,又會提示一個缺少必選的 name 屬性的錯誤。在這種情況下,就意味著上述代碼中交叉出來的 IntersectionTypeConfict 類型是一個無用類型。
如果同名屬性的類型兼容,比如一個是 number,另一個是 number 的子類型、數字字面量類型,合并后 name 屬性的類型就是兩者中的子類型。
如下所示示例中 name 屬性的類型就是數字字面量類型 2,因此,我們不能把任何非 2 之外的值賦予 name 屬性。

type IntersectionTypeConfict = { id: number; name: 2; }   
  & { age: number; name: number; }; 
  
  let mixedConflict: IntersectionTypeConfict = {  
    id: 1,  
    name: 2, // ok 
    age: 2 
  }; 
  mixedConflict = {   
    id: 1,  
    name: 22, // '22' 類型不能賦給 '2' 類型   
    age: 2  
  };

那么如果同名屬性是非基本數據類型的話,又會是什么情形。我們來看個具體的例子:

interface A {
  x:{d:true},
}
interface B { 
  x:{e:string},
}
interface C {  
  x:{f:number},
}
type ABC = A & B & C
let abc:ABC = { 
  x:{  
    d:true,  
    e:'',   
    f:666  
  }
}

以上代碼成功運行后,會輸出以下結果:
ts-node index.ts

{x:{d:true,e:'',f:666}}

由上圖可知,在混入多個類型時,若存在相同的成員,且成員類型為非基本數據類型,那么是可以成功合并。

10. 接口(Interfaces)

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

10.1. 什么是接口

在面向對象語言中,接口(Interfaces)是一個很重要的概念,它是對行為的抽象,而具體如何行動需要由類(classes)去實現(implement)。
TypeScript 中的接口是一個非常靈活的概念,除了可用于[對類的一部分行為進行抽象]以外,也常用于對「對象的形狀(Shape)」進行描述。

10.2. 簡單的例子

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

上面的例子中,我們定義了一個接口 Person,接著定義了一個變量 tom,它的類型是 Person。這樣,我們就約束了 tom 的形狀必須和接口 Person 一致。
接口一般首字母大寫。
定義的變量比接口少了一些屬性是不允許的:

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'.

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

10.3. 可選 | 只讀屬性

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

只讀屬性用于限制只能在對象剛剛創建的時候修改其值。此外 TypeScript 還提供了 ReadonlyArray<T> 類型,它與 Array<T> 相似,只是把所有可變方法去掉了,因此可以確保數組創建后再也不能被修改。

let a: number[] = [1, 2, 3, 4];
let ro: ReadonlyArray<number> = a;
ro[0] = 12; // error!
ro.push(5); // error!
ro.length = 100; // error!
a = ro; // error!

10.4. 任意屬性

有時候我們希望一個接口中除了包含必選和可選屬性之外,還允許有其他的任意屬性,這時我們可以使用 索引簽名 的形式來滿足上述要求。

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

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

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

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 的值卻是 number,number 不是 string 的子屬性,所以報錯了。
另外,在報錯信息中可以看出,此時 { name: 'Tom', age: 25, gender: 'male' } 的類型被推斷成了 { [x: string]: string | number; name: string; age: number; gender: string; },這是聯合類型和接口的結合。
一個接口中只能定義一個任意屬性。如果接口中有多個類型的屬性,則可以在任意屬性中使用聯合類型:

interface Person {  
    name: string;  
    age?: number; // 這里真實的類型應該為:number | undefined 
    [propName: string]: string | number | undefined;
}

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

10.5. 鴨式辨型法

所謂的鴨式辨型法就是像鴨子一樣走路并且嘎嘎叫的就叫鴨子,即具有鴨子特征的認為它就是鴨子,也就是通過制定規則來判定對象是否實現這個接口。
例子

interface LabeledValue { 
  label: string;
}
function printLabel(labeledObj: LabeledValue) {  
  console.log(labeledObj.label);
}
let myObj = { size: 10, label: "Size 10 Object" };
printLabel(myObj); // OK
interface LabeledValue { 
  label: string;
}
function printLabel(labeledObj: LabeledValue) {  
  console.log(labeledObj.label);
}
printLabel({ size: 10, label: "Size 10 Object" }); // Error

上面代碼,在參數里寫對象就相當于是直接給labeledObj賦值,這個對象有嚴格的類型定義,所以不能多參或少參。
而當你在外面將該對象用另一個變量myObj接收,myObj不會經過額外屬性檢查,但會根據類型推論為let myObj: { size: number; label: string } = { size: 10, label: "Size 10 Object" };,然后將這個myObj再賦值給labeledObj,此時根據類型的兼容性,兩種類型對象,參照鴨式辨型法,因為都具有label屬性,所以被認定為兩個相同,故而可以用此法來繞開多余的類型檢查。

10.6. 繞開額外屬性檢查的方式

鴨式辨型法
如上例子所示

類型斷言
類型斷言的意義就等同于你在告訴程序,你很清楚自己在做什么,此時程序自然就不會再進行額外的屬性檢查了。

interface Props {  
  name: string; 
  age: number;  
  money?: number;
}

let p: Props = { 
  name: "兔神", 
  age: 25, 
  money: -100000, 
  girl: false
} as Props; // OK

索引簽名

interface Props { 
  name: string;  
  age: number;  
  money?: number; 
  [key: string]: any;
}

let p: Props = { 
  name: "兔神", 
  age: 25, 
  money: -100000, 
  girl: false
}; // OK

11. 接口與類型別名的區別

實際上,在大多數的情況下使用接口類型和類型別名的效果等價,但是在某些特定的場景下這兩者還是存在很大區別。

TypeScript 的核心原則之一是對值所具有的結構進行類型檢查。而接口的作用就是為這些類型命名和為你的代碼或第三方代碼定義數據模型。

type(類型別名)會給一個類型起個新名字。type 有時和 interface 很像,但是可以作用于原始值(基本類型),聯合類型,元組以及其它任何你需要手寫的類型。起別名不會新建一個類型 - 它創建了一個新名字來引用那個類型。給基本類型起別名通常沒什么用,盡管可以做為文檔的一種形式使用。

11.1. Objects / Functions

兩者都可以用來描述對象或函數的類型,但是語法不同。
Interface

interface Point { 
  x: number; 
  y: number;
}

interface SetPoint { 
  (x: number, y: number): void;
}

Type alias

type Point = { 
  x: number; 
  y: number;
};

type SetPoint = (x: number, y: number) => void;

11.2. Other Types

與接口不同,類型別名還可以用于其他類型,如基本類型(原始值)、聯合類型、元組。

// primitive
type Name = string;

// object
type PartialPointX = { x: number; };
type PartialPointY = { y: number; };

// union
type PartialPoint = PartialPointX | PartialPointY;

// tuple
type Data = [number, string];

// dom
let div = document.createElement('div');
type B = typeof div;

11.3. 接口可以定義多次,類型別名不可以

與類型別名不同,接口可以定義多次,會被自動合并為單個接口。

interface Point { x: number; }
interface Point { y: number; }
const point: Point = { x: 1, y: 2 };

11.4. 擴展

兩者的擴展方式不同,但并不互斥。接口可以擴展類型別名,同理,類型別名也可以擴展接口。
接口的擴展就是繼承,通過 extends 來實現。類型別名的擴展就是交叉類型,通過 & 來實現。

接口擴展接口

interface PointX {   
    x: number
}

interface Point extends PointX {  
    y: number
}

類型別名擴展類型別名

type PointX = {   
    x: number
}

type Point = PointX & {  
    y: number
}

接口擴展類型別名

type PointX = {  
    x: number
}
interface Point extends PointX {  
    y: number
}

類型別名擴展接口

interface PointX { 
    x: number
}
type Point = PointX & {  
    y: number
}
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容