Typescript 實戰 --- (7)類型兼容性 和類型保護

一、類型兼容性
 
ts 允許類型兼容的變量相互賦值,這個特性增加了語言的靈活性
 
當一個 類型Y 可以被賦值給另一個 類型X 時,就可以說類型X兼容類型Y。其中,X被稱為“目標類型”,Y被稱為“源類型”
X兼容Y : X(目標類型) = Y(源類型)

 

1、結構之間兼容:成員少的兼容成員多的
 
基本規則是,如果 X 要兼容 Y,那么 Y 至少具有與 X 相同的屬性
interface Named {
  name: string;
}

let x: Named;
let y = { name: 'Chirs', age: 23 };

x = y;
console.log('x', x);   // x { name: 'Chirs', age: 23 }

// 這里要檢查 y 是否可以賦值給 x,編譯器檢查 x 中的每個屬性,看能否在 y 中也找到對應的屬性

// 相反,把 y 賦值給 x 就會報錯,因為 x 不具備 age 屬性

y = x;  // Property 'age' is missing in type 'Named' but required in type '{ name: string; age: number; }'

 

1-1、子類型賦值
let s: string = 'hello';

s = null; // 由于在 ts 中, null 是所有類型的子類型,也就是說 字符類型兼容null類型,所以可以賦值

 

1-2、接口兼容性
interface X {
  a: any;
  b: any;
}

interface Y {
  a: any;
  b: any;
  c: any;
}

let x: X = { a: 1, b: '2' }
let y: Y = { a: 3, b: 4, c: 5 }

// 只要源類型y 具備了 目標類型x 的所有屬性,就可以認為 x 兼容 y
x = y;
console.log('x', x);   // x { a: 3, b: 4, c: 5 }

 

2、函數之間兼容:參數多的兼容參數少的
 
需要判斷函數之間是否兼容,常見于兩個函數相互賦值的情況下,也就是函數作為參數的情況
 
2-1、如果要目標函數兼容源函數,需要同時滿足三個條件:
 
(1)、參數個數:目標函數的個數 多余 源函數的個數
interface Handler {
  (x: number, y: number): void
}

function foo(handler: Handler) {  // handler:目標函數
  return handler
}

let h1 = (a: number) => {}  // h1:源函數
// 目標函數的參數個數2個 > 源函數參數個數1個
foo(h1);

let h2 = (a: number, b: number, c: number) => {}  // h1:源函數
// 目標函數的參數個數2個 < 源函數參數個數3個
foo(h2);  // 類型“(a: number, b: number, c: number) => void”的參數不能賦給類型“Handler”的參數

 

(2)、參數類型:參數類型必須要匹配
interface Handler {
  (x: number, y: number): void
}

function foo(handler: Handler) {  // handler:目標函數
  return handler
}

let h3 = (a: string) => {}  // h3:源函數

// 盡管目標函數的參數個數多余源函數的參數個數,但是參數類型不同
foo(h3); 

/*
  報錯信息:
  類型“(a: string) => void”的參數不能賦給類型“Handler”的參數
  參數“a”和“x” 的類型不兼容
  不能將類型“number”分配給類型“string”
*/
interface Point3D {
  x: number;
  y: number;
  z: number;
}

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

// 函數 p3d 和 p2d 的參數個數都是1,參數類型都是對象
let p3d = (point: Point3D) => {}
let p2d = (point: Point2D) => {}

// 賦值時,依然采用的是目標函數的參數個數必須大于源函數參數個數,且參數類型相同的原則
p3d = p2d;
p2d = p3d; // 想要不報錯,需要關閉 tsconfig.json 中的一個配置  strictFunctionTypes

 

函數的參數之間可以相互賦值的情況,稱為 “函數參數雙向協變”。它允許把一個精確的類型,賦值給一個不那么精確的類型,這樣就不需要把一個不精確的類型斷言成一個精確的類型了
 
(3)、返回值類型:目標函數的返回值類型必須與源函數的返回值類型相同,或為其子類型
let p = () => ({ name: 'Bob' })
let s = () => ({ name: 'Bob', age: 23 })

// p 作為目標函數,s 作為源函數時,目標函數的返回值是源函數返回值的子類型
p = s;
s = p;  // 不能將類型“() => { name: string; }”分配給類型“() => { name: string; age: number; }”
 
2-2、關于固定參數、可選參數和剩余參數之間的兼容
 
1)、固定參數可以兼容可選參數和剩余參數
2)、可選參數不兼容固定參數和剩余參數
3)、剩余參數可以兼容固定參數和剩余參數
// 固定參數
let a = (x: number, y: number) => {};
// 可選參數
let b = (x?: number, y?: number) => {};
// 剩余參數
let c = (...args: number[]) => {};

// 固定參數 兼容 可選參數和剩余參數
a = b;
a = c;

// 可選參數 不兼容 固定參數和剩余參數 (可將 strictFunctionTypes 設為false 實現兼容)
b = a;
b = c;

// 剩余參數 兼容 固定參數和可選參數
c = a;
c = b;

 

2-3、函數重載
 
對于有重載的函數,源函數的每個重載都要在目標函數上找到對應的函數簽名,這樣確保了目標函數可以在所有源函數可調用的地方地方
// 源函數
function overload(x: number, y: number): number;
function overload(x: string, y: string): string;

// 目標函數
function overload(x: any, y: any): any{ };
// Error1: 目標函數的參數個數 少于 源函數的參數
// 源函數
function overload(x: number, y: number): number;  
// This overload signature is not compatible with its implementation signature
function overload(x: string, y: string): string;

// 目標函數
function overload(x: any, y: any, z: any): any{ };


// Error2: 目標函數和源函數的返回值類型不兼容
// 源函數
function overload(x: number, y: number): number;  
// This overload signature is not compatible with its implementation signature
function overload(x: string, y: string): string;

// 目標函數
function overload(x: any, y: any) { };

 

3、枚舉類型的兼容性
 
(1)、枚舉類型和數字類型相互兼容
(2)、枚舉類型之間是完全不兼容的
enum Color { Red, Green, Pink };
enum Fruit { Apple, Banana, Orange };

// 枚舉類型和數字類型相互兼容

let fruit: Fruit.Apple = 4;
let num: number = Color.Red;

// 相同枚舉類型之間不兼容
let c: Color.Green = Color.Red;
// 不能將類型“Color.Red”分配給類型“Color.Green”

// 不同枚舉類型之間不兼容

let color: Color.Pink = Fruit.Orange;
// 不能將類型“Fruit.Orange”分配給類型“Color.Pink”

 

4、類兼容性
 
(1)、靜態成員和構造函數是不參與比較的,如果兩個類具有相同的實例成員,那他們的實例則可以兼容
class A {
  id: number = 1;
  constructor(p: number, q: number) {}
}

class B {
  static s: number = 1;
  id: number = 2;
  constructor(p: number) {}
}

let aa = new A(3, 6);
let bb = new B(8);

// 兩個類都含有相同的實例成員 number 類型的id,盡管構造函數不同,依然相互兼容
aa = bb;
bb == aa;

 

(2)、如果兩個類中含有相同的私有成員,他們的實例不兼容,但是父類和子類的實例可以相互兼容
class A {
  id: number = 1;
  private name: string = 'hello';
  constructor(p: number, q: number) {}
}

class B {
  static s: number = 1;
  id: number = 2;
  private name: string = 'hello';
  constructor(p: number) {}
}

let aa = new A(3, 6);
let bb = new B(8);

// 在上例的基礎上各自添加了相同的 私有成員name,就無法兼容了
aa = bb;
bb == aa;

// 均報錯:不能將類型“B”分配給類型“A”,類型具有私有屬性“name”的單獨聲明
class A {
  id: number = 1;
  private name: string = 'hello';
  constructor(p: number, q: number) {}
}

class SubA extends A {}

let aa = new A(3, 6);
let child = new SubA(1, 2)

// 就算包含私有成員屬性,但是父類和子類的實例可以相互兼容
aa = child;
child == aa;

 

5、泛型兼容性
 
(1)、如果兩個泛型的定義相同,但是沒有指定泛型參數,它們之間也是相互兼容的;
// demo 1
interface Empty<T> {};

let a: Empty<string> = {};
let b: Empty<number> = {};

a = b;
b = a;


// demo 2
let log1 = <T>(x: T): T => {
  console.log('x');
  return x
}

let log2 = <U>(y: U): U => {
  console.log('y');
  return y;
}

log1 = log2;

 

(2)、如果泛型中指定了類型參數,會按照結果類型進行比較;
interface NotEmpty<T> {
  value: T;
};

let a: NotEmpty<string> = {
  value: 'string'
};
let b: NotEmpty<number> = {
  value: 123
};

a = b; // 不能將類型“NotEmpty<number>”分配給類型“NotEmpty<string>”

 

二、類型保護

 
此處定義了一個枚舉Type 和兩個類,兩個類都有打印的方法,在 getLanguage 函數中,我們希望通過傳入不同的參數,調用對應的打印方法
enum Type { Strong, Weak }

class Java {
  helloJava() {
    console.log('Hello Java')
  }
}

class JavaScript {
  helloJavaScript() {
    console.log('Hello JavaScript')
  }
}

function getLanguage(type: Type) {
  let lang = type === Type.Strong ? new Java() : new JavaScript();

  // Error:類型“Java | JavaScript”上不存在屬性“helloJava”
  if(lang.helloJava) {
    lang.helloJava()        // Error:類型“JavaScript”上不存在屬性“helloJava”
  } else {
    lang.helloJavaScript()  // Error:類型“Java”上不存在屬性“helloJavaScript”
  }

  return lang;
}

 

事實上,在上例中,變量lang被認為是一個聯合類型,意味著它必須同時具有 helloJava 和 helloJavaScript 兩個方法。此處為了解決報錯,就需要借助 類型斷言
function getLanguage(type: Type) {
  let lang = type === Type.Strong ? new Java() : new JavaScript();

  // 使用類型斷言
  if((lang as Java).helloJava) {
    (lang as Java).helloJava()        
  } else {
    (lang as JavaScript).helloJavaScript()  
  }

  return lang;
}

getLanguage(Type.Strong);    // Hello Java

 

由于不知道會傳入什么樣的參數,因此必須在每一處都加上類型斷言。顯然,這并不是一個理想的解決方案,代碼變得冗長且代碼的可讀性很差。
 
類型保護就是用來解決這個問題的,它可以提前對類型進行預判。
 
1、什么是類型保護
 
TypeScript 能夠在特定的區塊中保護變量屬于某種確定的類型,可以在此區塊中放心的引用此類型的屬性,或者調用此類型的方法。
 
2、創建特定區塊的方法:
 
(1)、instanceOf 判斷一個實例是不是屬于某個類
function getLanguage(type: Type) {
  let lang = type === Type.Strong ? new Java() : new JavaScript();

  // instanceOf
  if(lang instanceof Java) {
    lang.helloJava()        
  } else {
    lang.helloJavaScript()  
  }

  return lang;
}

 

(2)、in 判斷一個屬性是不是屬于某個對象
enum Type { Strong, Weak }

// 添加一個實例屬性,同時要添加構造器,否則在實例對象上還是找不到那個屬性
class Java {
  java: any; 
  constructor(java: any) {
    this.java = java;
  }

  helloJava() {
    console.log('Hello Java')
  }
}

class JavaScript {
  js: any;
  constructor(js: any) {
    this.js = js;
  }

  helloJavaScript() {
    console.log('Hello JavaScript')
  }
}

function getLanguage(type: Type) {
  let lang = type === Type.Strong ? new Java('java') : new JavaScript('js');

  // in
  if('java' in lang) {
    lang.helloJava()
  } else {
    lang.helloJavaScript()
  }

  return lang;
}

getLanguage(Type.Strong);    // Hello Java

 

(3)、typeof 判斷一個變量的類型
function getLanguage(x: string | number) {

  // typeof:此處只是提供一種創建類型保護區塊的方法,并不解決此例中的問題
  if(typeof x === 'string') {
    console.log(x.length)
  } else {
    console.log(x.toFixed(2));
  }

}

 

(4)、類型保護函數 某些判斷可能不是一條語句能夠搞定的,需要更多復雜的邏輯,適合封裝到一個函數內
enum Type { Strong, Weak }

class Java {
  helloJava() {
    console.log('Hello Java')
  }
}

class JavaScript {
  helloJavaScript() {
    console.log('Hello JavaScript')
  }
}

// 注意類型保護的返回值,是一個“類型謂詞”
function isJava(lang: Java | JavaScript): lang is Java {
  return (lang as Java).helloJava !== undefined
}

function getLanguage(type: Type) {
  let lang = type === Type.Strong ? new Java() : new JavaScript();

  // 類型保護函數
  if(isJava(lang)) {
    lang.helloJava()
  } else {
    lang.helloJavaScript()
  }

  return lang;
}

getLanguage(Type.Strong);    // Hello Java

 

posted @ 2020-01-19 10:48  rogerwu  閱讀(369)  評論(0編輯  收藏
最新chease0ldman老人