TypeScript真香系列-高級類型

前言

TypeScript真香系列的內容將參考中文文檔,可是文中的例子基本不會和文檔中的例子重複,對於一些地方也會深刻研究。另外,文中一些例子的結果都是在代碼沒有錯誤後編譯爲JavaScript獲得的。若是想實際看看TypeScript編譯爲JavaScript的代碼,能夠訪問TypeScript的在線編譯地址,動手操做,印象更加深入。javascript

交叉類型

交叉類型是將多個類型合併爲一個類型,至關於一種並的操做。java

interface IDog { 
    name: string,
    age: number,
}
interface ICat {
    name: string,
    color: string
}

let animal: IDog & ICat;
animal = {
    name: "哈士奇",
    age: 1,
    color: "white",
}
animal.name; // "哈士奇"
animal.age;  // 1
複製代碼

上面animal中的屬性一個都不能少,若是少了屬性的話,就會出現下面的錯誤: igit

nterface IDog { 
    name: string,
    age: number,
}
interface ICat {
    name: string,
    color: string
}

let animal: IDog & ICat;
animal = {   //錯誤,color屬性在ICat中是必須的
    name: "哈士奇",
    age: 1,
    // color: "white",
}
複製代碼

聯合類型

聯合類型能夠說是和交叉類型相反,聲明的類型不肯定,能夠是多個類型中的一個或幾個。github

let a: number | string;
a = 1;
a = "s";
a = false; // 錯誤,類型false不能分配給類型 number | string
複製代碼

看一個和交叉類型相對應的例子:typescript

interface IDog { 
    name: string,
    age: number,
}
interface ICat {
    name: string,
    color: string
}

let animal: IDog | ICat; // 這裏咱們把&改爲了|
animal = {     //沒有報錯
    name: "哈士奇",
    age: 1,
    // color: "white",
}
animal.name;
animal.age;
複製代碼

再看一個例子:函數

interface IDog { 
    name: string,
    age: number,
}
interface ICat {
    name: string,
    color: string
}

let animal: IDog | ICat;
animal = {
    name: "哈士奇",
    age: 1,
    color: "white",
}
animal.name;
animal.age; //錯誤,age不存在於ICat. age不存在於IDog | ICat
複製代碼

咱們能夠看見上面的例子出現了錯誤,這是由於TypeScript編譯器age不知道是IDog仍是ICat,因此只能訪問公共的name屬性。若是咱們想要訪問這個屬性的話,該怎麼辦?咱們可使用類型斷言:ui

interface IDog { 
    name: string,
    age: number,
}
interface ICat {
    name: string,
    color: string
}

let animal: IDog | ICat;
animal = {
    name: "哈士奇",
    age: 1,
    color: "white",
}
animal.name;
(<IDog>animal).age; // 1 複製代碼

這下就能訪問age屬性了。spa

類型保護

有時候咱們會遇到相似於下面這種場景:prototype

interface IDog { 
    name: string,
    age: number,
}
interface ICat {
    name: string,
    color: string
}

function animal(arg: IDog | ICat): any {
    if (arg.color) {     //錯誤
        return arg.color  //錯誤
    }
}
複製代碼

可是上面的代碼會出現錯誤。若是想要上面這段代碼正常工做,能夠和聯合類型中的例子同樣,使用類型斷言:code

interface IDog { 
    name: string,
    age: number,
}
interface ICat {
    name: string,
    color: string
}

function animal(arg: IDog | ICat):any {
    if ((<ICat>arg).color) {
        return (<ICat>arg).color;
    }
}
複製代碼

除了類型斷言,咱們還能夠利用類型保護來進行判斷,經常使用的類型保護有三種:typeof類型保護,instanceof類型保護和自定義類型保護。

typeof類型保護

function animal(arg: number | string): any {
    if (typeof arg === "string") {
        return arg + "貓";
    }
}
複製代碼

typeof類型保護只有兩種形式能被識別: typeof v === "typename"typeof v !== "typename"。"typename"必須是 "number", "string", "boolean"或 "symbol"。

instanceof類型保護

class Dog {
    name: string;
    age: number;
    constructor() { 

    };
}
class Cat {
    name: string;
    color: string;
    constructor() { 

    };
}

let animal: Dog | Cat = new Dog();
if (animal instanceof Dog) {
    animal.name = "dog";
    animal.age = 6;
}
if (animal instanceof Cat) {
    animal.name = "cat";
    animal.color = "white";
}
console.log(animal); //Dog {name: "dog", age: 6}
複製代碼

instanceof的右側要求是一個構造函數,TypeScript將細化爲:

  1. 此構造函數的 prototype屬性的類型,若是它的類型不爲 any的話;
  2. 構造簽名所返回的類型的聯合。

自定義類型保護

對於一些複雜的狀況,咱們能夠自定義來進行類型保護:

interface IDog { 
    name: string,
    age: number,
}
interface ICat {
    name: string,
    color: string
}
let animal: IDog | ICat;
animal = {
    name: "哈士奇",
    age: 6,
}
function isDog(arg: IDog | ICat): arg is IDog {

    return arg !== undefined;
    
}
if (isDog(animal)) {
    console.log(animal.age);  //6
}
複製代碼

類型別名

類型別名能夠給類型取一個別名。類型別名和接口相似,但又有不一樣。

type Name = number;
type Types = number | string;
type NAndT = Name & Types;
type MyFunc = () => number;

function animal(arg: Types) { 
    return arg;
}
animal("哈士奇"); //"哈士奇"
複製代碼

類型別名能夠做用於原始類型、聯合類型、泛型等等。

type Dog<T> = { value: T };

function dog(arg: Dog<string>) {
  return arg;
}

dog({ value: "哈士奇" }); //{value: "哈士奇"}
dog({ value: 1});  //錯誤,類型number不能分配給string
dog("哈士奇");  //錯誤,參數「哈士奇」不能分配給類型 Dog<string>
複製代碼

接口和類型別名的區別

區別一:接口能夠建立新的名字,並且能夠在其它任何地方使用;類型別名不建立新的名字,而是起一個別名。 區別二:類型別名能夠進行聯合,交叉等操做。 區別三:接口能夠被extends和implements以及聲明合併等,而類型別名不能夠。

這裏介紹一下聲明合併

「聲明合併」是指編譯器將針對同一個名字的兩個獨立聲明合併爲單一聲明。 合併後的聲明同時擁有原先兩個聲明的特性。 任何數量的聲明均可被合併;不侷限於兩個聲明。

舉個例子:

interface IDog {
    name: string;
    setName(arg:string): string;
}

interface IDog { 
    age: number;
}

interface IDog { 
    color: string;
}
let dog: IDog;
dog = {
    color: "black",
    age: 6,
    name: "哈士奇",
    setName: (arg) => { 
        return arg
    }
}
複製代碼

合併以後:

interface IDog { 
    color: string;
    age: number;
    name: string;
    setName(arg:string): string;
}
複製代碼

咱們能夠看出,後面的接口在合併後出如今了靠前的位置。

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

字符串字面量容許咱們指定字符串爲必須的固定值。

type Dog = "哈士奇" | "泰迪" | "中華田園犬" | "薩摩耶";

function dog(arg: Dog):any { 
  switch (arg) {
    case "哈士奇":
      return "傻狗";
    case "泰迪":
      return "精力旺盛";
    case "中華田園犬":
      return "忠誠";
    case "薩摩耶":
      return "微笑天使";
  }
}
dog("哈士奇");  //"傻狗"
dog("柯基");  //錯誤,參數"柯基"不能分配給類型Dog
複製代碼

數字字面量同理。

type Num = 1 | 2 | 3 | 4 | 5 | 6 | 7;

function week(arg: Num):any {
  switch (arg) {
    case 1:
      return "星期一";
    case 2:
      return "星期二";
    case 3:
      return "星期三";
    case 4:
      return "星期四";
    case 5:
      return "星期五";
    case 6:
      return "星期六";
    case 7:
      return "星期日";
  }
}
week(6);  //"星期六"
week(8);  //錯誤
複製代碼

可辨識聯合

咱們能夠合併單例類型、聯合類型、類型保護和類型別名來建立一個叫作可辨識聯合的高級模式。它具備三個要素:

  1. 有普通的單例類型屬性— 可辨識的特徵。
  2. 一個類型別名包含了那些類型的聯合— 聯合。
  3. 此屬性上的類型保護。

能夠看看下面這個例子就能理解了:

//咱們首先聲明瞭將要聯合的接口,目前各個接口之間是沒有聯繫的,
//只是都有一個kind的屬性(能夠稱爲可辨識特徵或標籤)可是有不一樣的字符串字面量類型
interface IColor {
  kind: "color";
  value: string;
}

interface ISize {
  kind: "size";
  height: number;
  width: number;
}

//而後咱們利用類型別名和聯合類型把兩個接口聯合到一塊兒
type MyType = IColor | ISize;

//最後使用可辨識聯合
function types(arg: MyType) :any{
  switch (arg.kind) {
    case "color":
      return arg.value;
    case "size":
      return arg.height * arg.width;
  }
}
types({ kind: "color", value: "blue" });  //"blue"
types({ kind: "size", height: 10, width: 20 }); //200
複製代碼

索引類型

使用索引類型,編譯器就可以檢查使用動態屬性名的代碼。咱們首先要知道兩個操做符。

  1. 索引類型的查詢操做符 keyof T,意爲:對於任何類型Tkeyof T的結果爲T上已知公共屬性的聯合;
  2. 索引訪問操做符T[K]
interface IDog{
  a: string,
  b: number,
  c: boolean,
}

let dog: keyof IDog; //let dog: "a" | "b" | "c"
let arg: IDog["a"];  //let arg: string
複製代碼

再看一個較爲複雜的例子:

interface IDog{
  name: string,
  age: number,
  value: string,
}

let dog: IDog;
dog = {
  name: "二哈",
  age: 6,
  value: "奧裏給"
}

function myDog<T, K extends keyof T>(x: T, args: K[]): T[K][] { 
  return args.map(i => x[i]);
}

myDog(dog, ['name']);  //["二哈"]
myDog(dog, ['name', 'value']);  //["二哈", "奧裏給"]
myDog(dog, ['key']);  //錯誤,類型不匹配
複製代碼

映射類型

有時候咱們能夠會遇到這種狀況,把每一個成員都變爲可選或者只讀:

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

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

interface PersonReadonly {
    readonly name: string;
    readonly age: number;
}
複製代碼

這在JavaScript裏常常出現,TypeScript提供了從舊類型中建立新類型的一種方式 — 映射類型。 在映射類型裏,新類型以相同的形式去轉換舊類型裏每一個屬性。

interface IPerson{
    name: string;
    agent: number;
}

type OPartial<T> = {
    [P in keyof T]?: T[P];
}

type OReadonly<T> = {
    readonly [P in keyof T]: T[P];
}
//使用方式
type PersonPartial = OPartial<IPerson>;
type ReadonlyPerson = OReadonly<IPerson>;
複製代碼

參考

github.com/zhongsp/Typ…

github.com/jkchao/type…

最後

文中有些地方可能會加入一些本身的理解,如有不許確或錯誤的地方,歡迎指出~

相關文章
相關標籤/搜索