TypeScript——兼具類型安全與輕便開發的靈魂語言

做者:DoubleJan

當咱們談到TypeScript時,咱們究竟在談什麼?TypeScript與JavaScript到底是什麼關係?TypeScript類型安全嗎?TypeScript類型那麼多,看不過來嗎?看了這篇文章,這些問題都能弄明白!javascript

一 TypeScript基本介紹

1. TypeScript類型

TypeScript是JavaScript的超集,是帶類型的JavaScript。css

TypeScript經過類型註解,來約束一個變量的類型。但類型註解是TypeScript層面的概念,被編譯後的JavaScript代碼不會存在「類型」,而且,類型錯誤不影響TypeScript編譯過程。能夠說,TypeScript的類型系統,僅僅在TypeScript環境中,給使用者類型提示或類型警告。java

2. TypeScript安裝和編譯

能夠經過npm來安裝TypeScriptgit

// 全局安裝typescript模塊

npm install -g typescript

// typescript使用tsc命令編譯文件或進行其餘操做

tsc demo.ts
複製代碼

3. TypeScript類型的特性(深刻理解TypeScript類型的用意)

(1)開發時(TS層)的類型約束。TS層的類型和報錯不影響編譯過程,不會存在於編譯後的JavaScript代碼中。 (2)基於結構類型的類型兼容性和類型推斷(區別於Java等語言的名義類型),只要兩個類型之間轉換時,不影響後續運算,那麼它們就是類型兼容的。 (3)結合編譯後的JavaScript代碼來學習TypeScript的特性,能夠更好的理解TypeScript。typescript

二 基礎類型與變量聲明

1. TypeScript支持的類型

TypeScript的基礎類型創建在JavaScript之上,兼容JavaScript的全部類型,包括布爾值(boolean),數字(number),字符串(string),數組([]),null(null),undefined(undefined),。npm

除此以外,TypeScript還額外支持一些類型,包括枚舉,元組,any,never,void。這不是說TypeScript包含非JavaScript的部分,實際上這些類型最終都被編譯爲JavaScript代碼,使用JavaScript方式實現。數組

JavaScript提供了void運算符,它是一元運算符,用來對錶達式進行求值,返回undefined。TypeScript不只支持void做爲操做符,也容許void做爲類型註解,表示空值。安全

2. 類型註解

除了元組,枚舉這兩種有複合或組合概念的類型之外,別的基本類型都經過{{:typename'}}的語法來進行類型註解。bash

let num: number;
let str: string;
let bool: boolean;
let strArr: string[];         // 數組的類型註解,只須要在任意類型後加上[]便可
let numArr: Array<number>;    // 也可使用數組泛型Array<type>
let nullValue: null;
let undefinedValue: undefined;
let anyValue: any;
let voidValue: void;
let neverValue: never;

// 元組的類型註解
let tuple: [string, number];

// 枚舉的類型註解
enum Color { Red, Blue };
let color: Color;
複製代碼

3. 元組類型

元組類型用來表示一個已知數量和類型的數組。各個元素的類型沒必要相同,但建立變量後,對應索引的元素類型必須一一對應,數組元素不能多,不能少。dom

4. 解構賦值

JavaScript中,解構賦值時容許對變量重命名,使用的是 {{val: tmp}}的形式,這與TypeScript的類型註解語法衝突了。所以,在解構賦值出現變量重命名時,首先知足JavaScript的語義要求。以後,能夠在解構的{{結構}}後面加上類型註解。

const obj = { x: 1, y: 2 }

const { x, y } = obj;
const { x: XX, y: YY }: { x: number, y: number } = obj;

console.log(x, y, XX, YY);  // 1 2 1 2
複製代碼

其實這裏的類型註解不是必須的,由於只要在TypeScript環境中定義的變量,一定要求是有類型的,也就是說,obj在作解構賦值時,內部屬性的類型已經明確了。此時,TypeScript能夠自行對類型作類型推斷。同理,在定義obj時,使用字面量語法,也是不須要顯式寫出類型註解的。

5. 展開

展開運算容許後續對象屬性覆蓋前面已經定義的屬性,但若是類型不一致,會產生報錯。

const defaults = { a: 'a', b: 'b' }
let search = { a: 'a', b: 2 }
search = { ...search, ...defaults }
// TypeScript error: Type '{ a: string; b: string; }' is not assignable to type '{ a: string; b: number; }'
複製代碼

6. any的賦值

any類型能夠接受任何類型值的賦值,也能夠接受了別的類型值後,賦值給不一樣類型。此時被賦值的變量類型相應變化。

any類型基本上等於關閉了TypeScript的類型檢查系統,應當少用。

7. 聲明

TypeScript中的聲明語句會建立三個實體中的一種:命名空間,類型或值。只有值是在JavaScript中可以實際看到的。聲明命名空間的語句會建立一個命名空間,聲明類型的語句會使用聲明的模型建立一個類型並綁定到給定的名字上,只有聲明一個值,纔會輸出在JavaScript代碼中。

三 枚舉類型

1. 默認枚舉(數值枚舉)

TypeScript經過{{enum}}關鍵字聲明一個枚舉類型,默認狀況下,值是數字類型。

enum CardSuit { Clubs = 1, Diamonds, Hearts, Spades }

// {1: "Clubs", 2: "Diamonds", 3: "Hearts", 4: "Spades", Clubs: 1, Diamonds: 2, Hearts: 3, Spades: 4}
複製代碼

枚舉類型可讓一組數據有一個跟友好的名稱。枚舉類型其實是由對象實現的。編譯成JavaScript代碼後,這個對象的屬性名和屬性值互爲鍵值對,即一組屬性名對應一組屬性值,同時這些屬性值也會是另外一組屬性的屬性名。

EnumObject[(EnumObject['key0'] = 0)] = 'key0';
EnumObject[(EnumObject['key1'] = 1)] = 'key1';
複製代碼

默認狀況下,用戶定義的第一個枚舉屬性值爲0,以後遞增,也能夠顯式地給第一個枚舉屬性定義數字值,後面的值會據此遞增。

注意事項: 能夠,也只能給第一個枚舉屬性顯式定義初始值。 通常能夠從1開始定義,避免0帶來的各類問題。 2. 非數值枚舉 TypeScript容許枚舉值的類型是其餘類型,好比說字符串。但若是使用非數字類型,如字符串,來定義枚舉,就必須把全部屬性都提供初始值。

enum CardSuit { Clubs = 'sd', Diamonds, Hearts, Spades }
// SyntaxError: Enum member must have initializer
複製代碼
  1. 常量枚舉 使用const關鍵字,能夠把枚舉定義成常量的。此時,爲了提高性能,TypeScript會將用到這個枚舉的地方直接初始化爲值,而不是枚舉對象引用,在運行時,這個常量枚舉對象不存在。

--preserveConstEnums編譯選項可讓編譯器在編譯時保留常量枚舉定義,從而使常量枚舉對象運行時存在。

四 類

1. 類型約束

Typescript支持對類的屬性和方法的類型約束。類的類型約束上,被賦值爲實例對象的變量,容許類型註解爲對象的父類,調用內部方法時,依然會調用當前對象的類的方法,不會直接調用父類同名方法。

class Animal {
  name: string;
  private id: string;
  protected code: string;

  constructor(animalName: string) {
    this.name = animalName;
    this.id = (Math.random() * 100000 / 100000).toString();
    this.code = `CODE${(Math.random() * 100000 / 100000)}`;
  }

  move(distanceInMeters: number = 0) {
    console.log(`${this.name} moved ${distanceInMeters}`);
  }
}

class Snake extends Animal {
  constructor(animalName: string) {
    super(animalName);
  }

  move(distanceInMeters: number = 0) {
    console.log('From Snake');
    super.move(distanceInMeters);
  }
}

const snake: Animal = new Snake('snake');
snake.move(30);    // From Snake   snake moved 30
複製代碼

在Javascript中,類的屬性默認是public,可顯式指定爲private,protected

2. 抽象類

ES6沒有提供抽象類,可是Typescript提供了抽象類。抽象類使用{{abstract}}關鍵字定義,抽象類的抽象方法不包含具體實現,且必須在派生類中實現。

abstract class Animal {
  abstract born(): void;
}
複製代碼

3. 類類型

當咱們定義了一個類的時候,得到的是類的實例類型以及類靜態資源。靜態資源包括構造函數和方法。類除了實例類型,還存在類類型,即構造函數的類型。構造函數內,包含了類的靜態屬性。使用類型註解{{: classname}},獲取的是實例類型,而若是想要獲取類類型(構造函數類型),須要使用{{typeof classname}}。

const greeter1: Greeter = new Greeter();
// Greeting: HELLO
console.log(greeter1.greet());

const greeterMaker: typeof Greeter = Greeter;
greeterMaker.standardGreeting = 'HELLO FROM MAKER';

const greeter2: Greeter = new greeterMaker();
// Greeting: HELLO FROM MAKER
console.log(greeter2.greet());

// object function HELLO FROM MAKER
console.log(typeof greeter1, typeof greeterMaker, greeterMaker.standardGreeting);
複製代碼

爲何類的類型就是構造函數類型呢?能夠在控制檯打印出一個實例對象,在原型屬性proto裏面找到constructor,它就被標註爲class

4. 用類作接口

類定義時會建立類的實例類型。即,類能夠建立出類型。所以,在容許使用接口時,也容許使用類。

class Point {
  x: number;
  y: number;

  // 若是類的屬性被定義卻沒有初始化,Typescript會報錯
  constructor(x: number, y: number) {
    this.x = x;
    this.y = y;
  }
}

interface Point3D extends Point {
  z: number;
}

let point: Point3D = { x: 1, y: 2, z: 3 }
複製代碼

詳細的內容,在接口裏面描述。

五 接口

1. 接口定義

TypeScript使用接口來對一個結構進行類型約束。接口的聲明使用{{interface}}關鍵字。爲接口的每個屬性進行類型註解。在繼承接口或使用接口做爲變量類型時,接口中屬性順序不定,但類型必須按屬性名一一對應,屬性不能多,不能少。

interface Point {
  x: number;
  y: number;
}
const point: Point = { y: 10, x: 3.56 }
const point3D: Point = { x: 12.4, y: 5.09, y: 23 }   // TypeScript error: Duplicate identifier 'y'
複製代碼

接口僅存在與TypeScript層,編譯成JavaScript後,接口是不存在的。

2. 帶只讀屬性的接口定義

接口定義時,可使用{{readonly}}關鍵字,來把屬性修飾爲只讀的,即在對象建立時初始化值,後續不容許再修改。

interface Point {
  // ...
  readonly origin: number
}

class P {
  readonly origin: number

  constructor(o: number) {
    this.origin = o;
  }

  // TypeScript error: Cannot assign to 'origin' because it is a read-only property
  setOrigin(o: number) {
    this.origin = o;
  }
}
複製代碼

readonly和const如何取捨?若是被註解的是變量,應當用const,若是被註解的是屬性,應當用readonly

3. 帶索引簽名的接口定義

若是一個接口存在許多不肯定的額外屬性,屬性名,屬性個數,都是不可預知的,可使用索引簽名來聲明這些屬性。

interface Square {
  color?: string;
  width: number;
  [propName: string]: any
}

const sq: Square = { width: 100, 1: 'square', borderd: false }
複製代碼

語句{{string: any}}意思是,容許接口接收除已經定義了的屬性(在Square中是color和width)外,額外的任意屬性,這些屬性的鍵名的類型爲string或number。接口內接收的全部屬性類型爲any,即任意一個類型的屬性均可以接收。

其中,方括號內的propName沒有實際意義,僅僅佔位,換成{{string}}也能夠。方括號內冒號後面表示屬性名的類型,可選的值爲string和number,由於在JavaScript中,屬性名只能是字符串或數字類型(數字實際上也是被轉化成字符串的)。選擇string實際上也容許屬性名類型爲number。最後的any表示整個接口範圍內,容許的全部的類型,這個類型註解也會約束前面已經定義了的屬性類型。

4. 帶函數的接口定義

接口中容許定義函數類型,只須要給函數的參數和返回值作類型註解便可。返回值爲空時,類型註解爲{{:void}},此時能夠省略。

interface Point {
  printPoint(desc: string): void
}

// 或者是這個樣子,這個時候只能實現爲函數
interface P {
  (desc: string): void
}
const p: P = (desc: string) => {
  console.log(`P: ${desc}`);
}
複製代碼

5. 類實現接口

普通類實現 TypeScript中,接口能夠被類繼承。類繼承接口時,必須包含接口定義的全部變量和函數。須要注意的是,接口描述的內容要求類進行公有繼承。若是把接口的變量繼承爲私有的會報錯。

TypeScript和JavaScript中,類的屬性和方法默認爲公有的。

// 接口實現
interface ClockInterface {
  currentTime: Date
}

class Clock implements ClockInterface {
  currentTime: Date = new Date();
  constructor(h: number, m: number) {}
  // private currentTime: Date = new Date();
  // Property 'currentTime' is private in type 'Clock' but not in type 'ClockInterface'
}
複製代碼

類類型實現 一個類實現接口時,只對實例部分進行檢查,所以,靜態部分,好比構造函數是不會被檢查的。若是接口要求實現構造函數,須要額外的寫法。首先,構造函數的類型註解格式爲:new (): 。

// 構造函數類型,即,類類型
interface ClockConstructor {
  new (hour: number, minute: number): ClockInterface;
}

interface ClockInterface {
  tick(): void;
}

function createClock(ctor: ClockConstructor, hour: number, minute: number): ClockInterface {
  return new ctor(hour, minute);
}

class DigitalClock implements ClockInterface {
  hour: number;
  minute: number;
  constructor(h: number, m: number) {this.hour = h; this.minute = m;}
  tick() {
    console.log(`it is ${this.hour}: ${this.minute} clock`);
  }
}

let digital = createClock(DigitalClock, 12, 25);

複製代碼

類表達式 繼承接口,或定義類,可使用類表達式語法,這樣會簡潔一些。

interface ClockConstructor {
  new(hour: number, minute: number): ClockInterface;
}

interface ClockInterface {
  tick(): void;
}

const Clock: ClockConstructor = class Clock implements ClockInterface {
  constructor(h: number, m: number) { }
  tick() { console.log('beep') }
}
複製代碼

6. 繼承與合併接口

接口繼承 一個接口可使用{{extends}}繼承其餘接口。

interface Shape {
  color: string
}

interface Square extends Shape {
  sideLength: number;
}
複製代碼

接口合併 能夠重複聲明同名接口,這時接口會合並,而且包含全部聲明的部分。

interface Point {
  x: number;
  y: number;
  z?: number;
}
interface Point {
  // ...
  readonly origin: number
}
interface Point {
  printPoint(desc: string): void
}
複製代碼

7. 接口繼承類

接口也能夠繼承類。接口容許繼承類的全部成員,但不包括實現(不管是屬性的賦值仍是方法實現)。若是類具備靜態部分和私有或受保護成員,那就必需要同時繼承類,由於接口不檢查靜態,私有,和受保護部分

class Control {
  private state: any;
}

interface SelectableControl extends Control {
  select(): void
}

class Button extends Control implements SelectableControl {
  select() {}
}

// Property 'state' is missing in type 'Image' but required in type 'SelectableControl'
class Image implements SelectableControl {
  select() {}
}
複製代碼

六 函數

1. 類型註解

Typescript要求函數提供類型註解。函數的類型註解包含兩部分,參數和返回值的類型註解。一個完整的函數類型註解相似於這樣:

let fun: (x: number, y: string) => string = function(x: number, y: string): string { return x + y }
複製代碼

基於TypeScript的類型推斷機制,若是使用這樣的函數聲明並賦值給變量,那麼左右兩側的類型註解只寫一側便可。

當沒有返回值時,類型爲{{:void}},也能夠省略不寫。

interface Args {
  key: number
}

function returnArgs (arg: Args): Args {
  return arg;
}

function printArgs (arg: Args) {
  console.log('args: ', arg);
}
複製代碼

2. 參數約束

Typescript中,函數的全部參數都是必須的。形參個數必須與實參數量一致,類型一致。若是沒有值,可使用可選參數,寫法爲{{arg?: }},也能夠給可選參數指定默認值。若是函數參數個數不定,可使用rest參數(剩餘參數)。

須要注意的是,沒有默認值的可選參數,rest參數,都是不定的,所以不能放在固定參數,有默認值的可選參數的前面,可選參數之間,不管是否有默認值,位置上不強制要求。rest參數必須放在全部參數的最後面。

interface Comples {
  (str: string, isPrint?: boolean, num?: number,  ...args: boolean[]): number
}

// args必須使用rest風格,即...開頭,不然只會接收到剩餘參數的第一個
const comples: Comples = (str, isprint = true, num, ...args) => {
  if (isprint) {
    console.log(`argumenst: ${str}, ${isprint}, ${num}, ${args}`)    
  }
  return str.length;
}

// argumenst: comples, true, 4, true,false,true
// 7
console.log(comples('comples', undefined, 4, true, false, true
複製代碼

3. this類型約束

在JavaScript中,類型是沒有約束的。對於this值,也只能本身跟蹤。假設一個對象同時也表明着一種類型,那麼假如在函數調用時出現了不一樣於定義時的this對象類型,就能夠顯式的報錯。Typescript容許在函數參數列表的第一個位置,顯式指定this的類型,來約束this類型,也就_約等於_約束了this的指向。這只是一種約束形式,不會要求調用函數時,把this傳遞進來。

之因此是約等於,是由於本質上,Typescript約束的是類型,而不是實例。所以,一個同類型的不一樣對象,Typescript不能在編譯時檢查出來。但我的感受同類型不一樣實例的狀況比較罕見。

// 使用bind綁定一個同類型的新對象
const res = d1.createCardPicker.bind({
  suits: ['hearts', 'spades'],
  cards: Array(52),
  createCardPicker: function (this: Deck) {
    return () => {
      const pickedCard = Math.floor(Math.random() * 52);
      return { suit: this.suits[3], card: pickedCard % 13 };
    }
  }
});

// res: {suit: undefined, card: 12}
console.log('res: ', res()());  
複製代碼

4. 函數重載

TypeScript容許聲明多個不一樣參數的同名函數,並在最後一個函數聲明後實現函數,這被稱爲函數重載。若是不一樣的重載函數參數個數不一樣,多出來的參數必須是可選的。

function printf(x: number, y: string): void; function printf(x: string, y: string, z?: boolean): void; function printf(x: any, y: any) {
  console.log('x, y', x, y);
}

export default () => {
  printf(1, 'yi');
  printf('er', '二');
  return null;
}
複製代碼

函數重載只在TypeScript層出現,編譯後的JavaScript代碼不存在函數重載,所以不會形成運行時性能損耗

exports.__esModule = true;
function printf(x, y) {
    console.log('x, y', x, y);
}
exports["default"] = (function () {
    printf(1, 'yi');
    printf('er', '二');
    return null;
});
複製代碼

5. undefined的函數傳參

any類型兼容全部類型,也包括null,undefined,和any自己。可是,在函數傳參時,參數缺失不等於傳遞{{undefined}}類型

const anyArch = (person: any) => {
  if (person == null) {
    people.push({
      Name: { firstName: 'AA', lastName: 'BB' },
      code: 12,
      isBad: true,
      area: ['guangdong', 'guangzhou', 'tianhe']
    });
  }
}

anyArch(undefined);  // success
anyArch();           // error: Expected 1 arguments, but got 0

複製代碼

七 泛型

1. 泛型變量

有時候咱們須要讓組件或函數,不只可以支持當前的數據類型,還但願可以支持其餘更多的類型,使用any能夠勉強解決這個需求,可是類型檢查就不存在了。TypeScript提供了相似於Java等語言的泛型支持。使用泛型變量來支持泛型的類型約束。

function printf<T>(arg: T):T {
  console.log('arg<type>: ', arg, typeof T);
  return arg;
}
複製代碼

注意事項: 須要注意的是,{{T}}或者說類型變量,不能做爲實際變量使用,例如:

typeof T  // 'T' only refers to a type, but is being used as a value here
複製代碼

使用泛型時,可使用{{}}的方式顯式聲明類型。也能夠藉助TypeScript的類型推斷能力。

printf<number>(34);
printf(34)   // 類型推斷
複製代碼

2. 泛型變量數組

泛型也容許使用類型變量數組。聲明類型變量寫法不變,只要使用到的時候在類型變量後面加上一對方括號,或使用Array泛型便可,例如{{T[]}},Array

function printList<T>(arg: T[]):T[] {
  console.log('arg<type>: ', arg);
  return arg
}
printList<number>([34, 56])
複製代碼

3. 泛型接口

聲明一個泛型接口,不須要對聲明語句有特殊寫法,只須要在內部根據須要使用泛型變量定義屬性和方法便可。與聲明普通的變量和方法沒有什麼區別

interface GenericIdentity {
    <T>(arg: T): string;
}
複製代碼

4. 泛型類

泛型類寫法與泛型接口差很少。泛型類使用{{<>}}括起泛型類型,跟在類名後面。

class GenericNumber<T> {
    zeroValue: T;
    add: (x: T, y: T) => string
}
const mGenericNumber = new GenericNumber<number>();
複製代碼

5. 泛型約束

使用了泛型之後,不能隨意使用泛型的屬性,由於泛型的具體類型是不肯定的。爲了保證某一屬性一定存在,可讓泛型變量繼承一個接口。

interface Lengthwise {
    length: number
}
function loggingIdentity<T extends Lengthwise> (arg: T): string {
    return `arg<${typeof arg}>: ${JSON.stringify(arg)}`;
}
loggingIdentity([3, 7, 5]);
複製代碼

6. 使用類型參數

若是須要從泛型類型中,拿到某一個屬性的類型,可使用索引查詢操做符{{keyof}}。經過{{keyof}}來獲取一個類型的某一屬性的類型。

// K extends keyof T 這裏extends表示K的類型應該來源於T的某一屬性類型
function printProperty<T, K extends keyof T>(obj: T, key: K) {
    return obj[key];
}
複製代碼

7. 使用類類型的泛型參數約束

前面提到的使用{{extends}}關鍵字,約束左值從右值派生出來。不只能做用於屬性和類之間,也能夠在兩個繼承關係的類之間使用。從而約束一個類型參數,必須來源於另外一個類型參數。

class Animal {
    name: string;
    constructor(n: string) {
        this.name = n;
    }
}

class Cat extends Animal {
    keeper: CatKeeper
    constructor(name) {
        super(name);
        this.keeper = new CatKeeper();
        this.keeper.count = 1;
    }
}

function createInstance<A extends Animal>(obj: new (n: string) => A, name: string): A {
    return new obj(name);
}
複製代碼

八 類型推斷與類型兼容

1. 最佳通用類型

當須要從幾個表達式中推斷類型的時候,會使用這些表達式的類型來推斷出一個最合適的通用類型。默認狀況下,會檢查全部的表達式:

檢查它們是否有共同的類型,好比是否都有同一個父類。若是有,就會被推斷爲該類型 若是沒有共同的類型,好比所有是基礎類型,默認會使用聯合類型,形如{{string | number}}。

2. 上下文歸類

若是當前表達式所須要的類型,在以前已經定義過,或者根據程序邏輯,這裏的類型是明確的,此時,TypeScript會採用上下文歸類的原則,推斷出這裏的類型。

典型的,好比匿名函數賦值給一個變量,那麼類型定義只須要寫一遍,或者當一個函數返回一個數字,那麼接收這個返回值的變量也是不用定義類型的。

const fn = (num: number, isNum: boolean): string => `${num} is number ?: ${isNum}`
複製代碼

3. 結構類型

TypeScript的類型系統是基於結構類型的。結構類型區別於Java爲表明的名義類型。在Java這種基於名義類型的語言中,即使兩個類具備徹底相同的屬性,只要是分別定義的不一樣類,它們的實例就是不一樣的類型。但TypeScript不一樣,TypeScript的類型管理基於結構類型,只要兩個結構體具備相同的屬性,那麼類型就是相同的。

對於結構類型的類型兼容來講,一個結構x要想類型兼容另外一個結構y,至少y具備與x相同的屬性。

interface Named {
    name: string
}

class Person {
    name: string
    constructor(name: string) {
        this.name = name;
    }
}
const p: Named = new Person('username');
`Person name<${typeof p.name}>: ${p.name}`;  // Person name<string>: username
複製代碼

4. 函數的參數類型兼容

若是一個函數x的參數列表中,每個參數的位置和類型,都能在另外一個函數y的參數列表中一一對應(y中多餘的參數不作限制),那麼x就是對y參數類型兼容的。簡單來講就是,參數需求少的函數,容許在提供更多的參數的函數類型上使用。

典型的應用場景就是,Array的原型方法,map, forEach等等的回調函數中,它們被定義爲接收三個參數,當前遍歷的元素,元素索引,整個數組,但一般提供只接收前幾個的參數的回調函數。

let x = (a: number, b: number) => a - b;
let y = (b: number, increment: number, c: string) => b + increment;

// x的全部參數都能在y裏面找到
y = x;
複製代碼

5. 函數的返回值類型兼容

一個函數x的返回值類型,若是是另外一個函數的返回值類型的子類型,即x的返回值對象的每個屬性都能在y返回值中找到(y中多餘的屬性不作限制),那麼x是對y返回值類型兼容的。簡單來講就是,提供返回值更少的兼容更多的。

let x = () => ({ name: 'Double' });
let y = () => ({ name: 'Float', location: 'Home' });

// x的返回值的屬性,在y中全存在
x = y;
複製代碼

6. 可選參數和剩餘參數

可選參數和剩餘參數不影響類型兼容的判斷,容許目標函數的可選參數不在源函數的參數列表中,也容許源函數的可選參數不在源函數的參數列表中。剩餘參數被看成是無限個可選參數。

function handler(args: number[], callback: (...args: number[]) => number ) {
    return callback(...args);
}
handler([2, 4, 6], (x, y) => x + y);
handler([1, 3, 5], (...args) => args[2] - args[0])}, ${handler([1, 0], (...args) => args[2] - args[0])
複製代碼

7. 枚舉類型的類型兼容

枚舉類型與數字類型或字符串類型是兼容,這字符串仍是數字,這取決於使用者給枚舉類型定義的值的類型。不一樣枚舉類型之間不兼容。

enum Status { Ready, Waiting }
enum Color { Red, Green, Blue }
enum Select { NoA = 'AAA', NoB = 'BBB' }

const status = Status.Ready;
const str: string = Select.NoA;
// `status ==? Color.Red ${status == Color.Red}` // TypeScript error

// const num: number = Select.NoA; // TypeScript error 
console.log(`status: ${status}, Select NoA: ${str}`);  // status: 0, Select NoA: AAA
複製代碼

8. 類的兼容性

檢查類的兼容性時,只考慮實例部分,靜態成員和構造函數不影響類的類型兼容性。

class Animal {
    name: string;
    constructor(name: string, age: number) {
        this.name = name;
    }
}
class Person {
    name: string;
    constructor(name: string) {
        this.name = name;
    }
}
let a: Animal = new Animal('a animal', 5);
let p: Person = new Person('a person');
a = p;  // OK
複製代碼

類的私有和保護成員會影響類型兼容性

class Animal {
    name: string;
    protected age: number;
    constructor(name: string, age: number) {
        this.name = name;
        this.age = age;
    }
}

class Person {
    name: string;
    protected age: number;
    constructor(name: string, age: number) {
        this.name = name;
        this.age = age;
    }
}

let a: Animal = new Animal('a animal', 5);
let p: Person = new Person('a person', 22);
// 私有成員來自不一樣類,類型不兼容
 a = p;    // TypeScript error
複製代碼

9. 泛型的類型兼容性

泛型的寫法通常爲{{TypeName}},T就是一個泛型變量。在使用泛型時,咱們一般是傳入一個類型,返回一個泛型包裝的結果類型。也就是說,真正影響到後續程序和運算的,是結果類型。所以,泛型的類型兼容性判斷上,僅考慮結果類型,即使泛型名稱,泛型變量不一樣,只要結果類型一致,那麼它們就是類型兼容的。

interface GenericInterface<T> {
    data: T
}

let x: GenericInterface<number>;
let y: GenericInterface<string>;
x = y;     // TypeScript error

interface EmptyInterface<T> { }

let a: EmptyInterface<number>
let b: EmptyInterface<string>

a = b;
a = 34;
複製代碼

九 高級類型

1. 交叉類型

交叉類型寫法相似於{{T & U}},用於將多個類型合併爲一個類型。交叉類型要求同時知足全部指定的類型的要求。也就是全部類型的並集(包含全部屬性)。若是函數的返回值是交叉類型,必須作顯式類型轉換(類型斷言)。若是幾個類型中有同名屬性,後面的屬性值會覆蓋前面的屬性值。

function extend<First, Second>(first: First, second: Second): First & Second {
    const result: First & Second = { ...first, ...second }
    return <First & Second>result; } class Cat { name: string; catchMouse: boolean; constructor(n, c) { this.name = n; this.catchMouse = c; } } class Dog { name: string; walkDog: boolean; constructor(n, w) { this.name = n; this.walkDog = w; } } const result = extend({ name: 'a cat', catchMouse: false }, { name: 'a dog', walkDog: true }); 複製代碼

2. 聯合類型

聯合類型用於在一堆備選類型中,選擇任意一個類型。它的出現是爲了某些時候確實須要考慮不一樣類型,但使用any又丟失了類型約束,所以使用聯合類型,將容許的類型羅列出來。

function unionAnimal(animal): Cat | Dog {
    const result = { ...animal };
    return <Cat | Dog>result; } class Cat { name: string; catchMouse: boolean; constructor(n, c) { this.name = n; this.catchMouse = c; } } class Dog { name: string; walkDog: boolean; constructor(n, w) { this.name = n; this.walkDog = w; } } 複製代碼

// 這不能用類型斷言,強制轉換成Cat類型 const result = unionAnimal({ name: 'a cat', catchMouse: false });

// 只能訪問聯合類型共有成員 // union: ${result.name}, ${result.walkDog}, ${result.catchMouse}; // TypeScript error console.log(union: ${JSON.stringify(result)});

## 3. 類型守衛與類型謂詞
以上面的聯合類型爲例,通常狀況下,只能訪問聯合類型的特有成員,而且不能用強制類型轉換,將聯合類型轉換成某一備選類型。此時,就須要使用類型守衛和類型謂詞,來確認聯合類型的結果類型,一定是某一備選類型。

類型謂詞使用{{is}}關鍵字,形如{{t is T}},來判斷某一變量是不是某個類型。咱們能夠定義一個函數,返回一個類型謂詞,用來判斷變量的類型,這個函數就是一個類型守衛。
```javascript
class Cat {
    name: string;
    catchMouse: boolean;
    constructor(n, c) {
        this.name = n;
        this.catchMouse = c;
    }
}

class Dog {
    name: string;
    walkDog: boolean;
    constructor(n, w) {
        this.name = n;
        this.walkDog = w;
    }
}

// 自定義的類型守衛,返回一個類型謂詞
function isCat(animal: Cat | Dog): animal is Cat {
    return (<Cat>animal).catchMouse !== undefined;
}

// 若是這裏定死了Cat,TypeScript會知道,永遠進不去else分支,so就不能使用walkDog
// 此時else分支被認爲是never的: Property 'walkDog' does not exist on type 'never'
const result = Math.random() < 0.5 ? new Cat('a cat', false) : new Dog('a dog', true);

// TypeScript不只知道在if分支裏是Cat類型; 它還清楚在else分支裏,必定不是Cat類型,必定是Dog類型
if (isCat(result)) {
    console.log(`result is ${JSON.stringify(result)}`);
} else {
    console.log(`another result ${result.walkDog ? 'can' : 'can\'t'} walk the dog`);
}
複製代碼

4. typeof類型守衛

當typeof操做符是按如下兩種方式被使用的時候:

typeof v === 'typename'
typeof v !== 'typename'
typeof被當作是一個類型守衛,不須要提供函數實現,直接使用便可。此時,typename必須是{{number}}, string, boolean, {{symbol}}的其中一種。typeof與其餘類型或字符串也能夠比較,此時不會被當作類型守衛。

// 能夠將typeof類型守衛定義爲一個函數
function isNumber(x: any): x is number {
    return typeof x === 'number';
}
// 直接使用typeof,會被默認爲類型守衛
console.log(`'sdd' is Number: ${isNumber('sdd')}, 'sdd' is string: ${typeof 'sdd' === 'string'}`);
複製代碼

5. instanceof類型守衛

在JavaScript中,instanceof操做符的做用是,檢測一個構造函數的原型是否存在於某一個實例對象的原型鏈上。在TypeScript中,instanceof依然是這個做用,而且,它被默認的看成是類型守衛。

class Animal {
    name: string;
    constructor(n) { this.name = n; }
}

class Pet extends Animal {
    constructor(n) { super(n); }
}

class Dog extends Pet {
    walkDog: boolean;
    constructor(n, w) {
        super(n);
        this.name = n;
        this.walkDog = w;
    }
}

const pet = new Pet('a pet') instanceof Animal;
const dog = new Dog('a dog', false) instanceof Animal;

console.log(`pet is animal: ${pet}, dog is Animal: ${dog}`);
複製代碼

6. 類型別名

類型別名使用{{type}}關鍵字來聲明,顧名思義,它是給一個類型或類型表達式定義一個別名。類型別名的使用時機,能夠參考如下幾點:

基礎類型沒有必要使用類型別名,沒有意義。 類型別名與接口是有區別的。類型別名不會建立一個真實的名字,只是建立一個引用。接口定義並建立了一個名字,而且接口能夠被繼承,而類型別名不能夠。 當使用多個類型的聯合類型或交叉類型時,應當定義一個類型別名,提高程序可讀性。 類型別名僅能用於類型註解,不能當作變量使用。

type Name = string;
type GetName = <T>(param: T) => string;
type Age = number;
type Info = Name & Age;  // 一個變量不可能同時是字符串和數字類型,所以這行建立的Info的別名爲never類型
type OneOf = Name | Age;
const getName: GetName = function <T>(param: T): string {
    return JSON.stringify(param);
}
const name: Name = 'a name';
const age: Age = 25;
const info: Info = 25;    // TypeScript error
const oneof: OneOf = 11;

console.log(`${name}'s age is ${age} or ${oneof}, is never: ${getName({ never: true })}`);
複製代碼

7. 數字和字符串字面量類型

字符串和數字的字面量類型,用於給變量規定字符串或數字的可選值的範圍。

type AB = 'A' | 'B';
type BIN = 1 | 0;
const ab: AB = 'C';    // TypeScript error
const bin = 1;

console.log(`BIN: ${bin}`);
複製代碼

8. 索引類型

若是一個變量是另外一個變量的一個屬性,能夠經過索引類型查詢操做符{{keyof}}和索引訪問操做符{{[]}}進行類型註解和訪問。

// T是一個任意類型,K類型是T類型中,任意一個屬性的類型,形參names是K類型變量組成的數組
// 返回值 T[K][]: T類型的K屬性數組(第一個方括號表示取屬性,第二個表示數組類型)
function pluck<T, K extends keyof T>(o: T, names: K[]): T[K][] {
    return names.map(n => o[n]);
}

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

const person: Person = {
    name: 'doublejan',
    age: 17
}

const strs: string[] = pluck(person, ['name']);
console.log(`Person Name: ${strs}`);
複製代碼

9. 映射類型

(1) 同態映射 對於一些屬性,咱們但願它們可以有公共的約束,好比,一個對象的一些屬性是可選的,或是隻讀的。固然能夠一個個的設置,可是更優雅的方式是使用映射類型,將舊類型用相同的方式轉換出來一批新類型。

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

// 這裏使用了索引查詢操做符 keyof 把P變量的類型綁定爲T的屬性類型
// 又使用索引簽名的語法 [prop: propType]: <type>,匹配到傳進來的泛型T的全部屬性
// 這種映射被稱爲 同態映射 ,由於全部的映射都是發生在類型T之上的,沒有別的變量和屬性參與
type Readonly<T> = {
    readonly [P in keyof T]: T[P];
}
type Partial<T> = {
    [P in keyof T]?: T[P];
}

const pPartial: Partial<Person> = { name: 'only name' }
const pReadonly: Readonly<Person> = { name: 'const name', age: 32 }
複製代碼

(2) 層疊映射 映射就像css同樣,是能夠層疊的,編譯器在聲明新的類型前,會拷貝已經存在的全部類型修飾符。假如某一類型的全部屬性被映射爲可選的,此時再通過只讀映射包裝,那麼全部的類型就是可選且只讀的。

10. 條件類型

普通的有條件類型 有條件的類型會以一個條件表達式進行類型關係檢測,從而在兩個類型中任選其一。條件類型的寫法相似於{{T extends U ? X : Y}}或者解析爲x,或者解析爲y,再或者延遲解析。條件類型不能轉換成任意一個備選的類型。

// return `${f(Math.random() < 0.5)}`;
// TypeName<T>是一個條件類型,用於檢測T的類型,返回一個類型明確的類型字面量
type TypeName<T> =
    T extends string ? string :
    T extends number ? number :
    T extends boolean ? boolean :
    T extends undefined ? undefined :
    T extends () => string ? () => string :
    T extends Function ? () => void :
    object;

// 如下type關鍵字定義的類型,通過TypeName<T>的條件類型檢測,由返回的類型,生成對應的類型別名.
type T0 = TypeName<string>;  // "string"
type T1 = TypeName<"a">;  // "string"

type T3 = TypeName<() => string>;  // "function"
type T4 = TypeName<() => void>;
type T5 = TypeName<string[]>;  // "object"

const fn: T3 = () => 'T3 is Function Type';
const fnVoid: T4 = function () { }
const str: T0 = 'string type';

console.log(`${fn()}, ${fnVoid()}, ${str}`);
分佈式條件類型
分佈式有條件類型在實例化時會自動分發爲聯合類型。例如,實例化{{T extends U ? X : Y}},T的類型爲{{A | B | C}},會被解析爲{{(A extends U ? X : Y) | (B extends U ? X : Y) | (C extends U ? X : Y)}}

type TypeName<T> =
    T extends string ? string :
    T extends number ? number :
    T extends boolean ? boolean :
    T extends undefined ? undefined :
    T extends () => string ? () => string :
    T extends Function ? () => void :
    object;

type T12 = TypeName<string | string[] | undefined>;

const obj: T12 = { key: 'value' }
console.log(`obj: ${JSON.stringify(obj)}`);
複製代碼
相關文章
相關標籤/搜索