Typescript高階型別

JRucker發表於2019-02-26

Typescript:高階型別

物件的型別

在 TypeScript 中,我們使用介面(Interfaces)來定義物件的型別。

什麼是介面

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

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

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

let man: Person = {
  name: 'Xcat Liu',
  age: 25,
};
複製程式碼

上面的例子中,我們定義了一個介面 Person,接著定義了一個變數 man,它的型別是 Person。這樣,我們就約束了 man的形狀必須和介面 Person 一致。

介面一般首字母大寫

定義的變數比介面少了一些或者多了一些屬性是不允許的:

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

let xcatliu: Person = {
  name: 'Xcat Liu'
};

// 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 xcatliu: Person = {
  name: 'Xcat Liu',
};
複製程式碼
interface Person {
  name: string;
  age?: number;
}

let xcatliu: Person = {
  name: 'Xcat Liu',
  age: 25,
};
複製程式碼

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

這時仍然不允許新增未定義的屬性:

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

let xcatliu: Person = {
  name: 'Xcat Liu',
  age: 25,
  website: 'http://xcatliu.com',
};

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

任意屬性

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

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

let xcatliu: Person = {
  name: 'Xcat Liu',
  website: 'http://xcatliu.com',
};
複製程式碼

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

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

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

let xcatliu: Person = {
  name: 'Xcat Liu',
  age: 25,
  website: 'http://xcatliu.com',
};

// index.ts(3,3): 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; website: 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 的子屬性,所以報錯了。

只讀屬性

有時候我們希望物件中的一些欄位只能在建立的時候被賦值,那麼可以用 readonly 定義只讀屬性:

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

let xcatliu: Person = {
  id: 89757,
  name: 'Xcat Liu',
  website: 'http://xcatliu.com',
};

xcatliu.id = 9527;

// index.ts(14,9): error TS2540: Cannot assign to 'id' because it is a constant or a read-only property.
複製程式碼

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

只讀的約束存在於第一次給物件賦值的時候,而不是第一次給只讀屬性賦值的時候:

陣列的型別

「型別 + 方括號」表示法

最簡單的方法是使用「型別 + 方括號」來表示陣列:

let fibonacci: number[] = [1, 1, 2, 3, 5];
複製程式碼

陣列的項中不允許出現其他的型別:

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

// index.ts(1,5): error TS2322: Type '(number | string)[]' is not assignable to type 'number[]'.
//   Type 'number | string' is not assignable to type 'number'.
//     Type 'string' is not assignable to type 'number'.
複製程式碼

陣列泛型

也可以使用陣列泛型(Generic) Array 來表示陣列:

let fibonacci: Array<number> = [1, 1, 2, 3, 5];
複製程式碼

any 在陣列中的應用

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

let list: any[] = ['Xcat Liu', 25, { website: 'http://xcatliu.com' }];
複製程式碼

函式型別

函式宣告

在 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 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 的型別定義中,=> 用來表示函式的定義,左邊是輸入型別,需要用括號括起來,右邊是輸出型別。

介面中函式的定義

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

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

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

可選引數

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

與介面中的可選屬性類似,我們用 ? 表示可選的引數:

function buildName(firstName: string, lastName?: string) {
  if (lastName) {
    return firstName + ' ' + lastName;
  } else {
    return firstName;
  }
}
let xcatliu = buildName('Xcat', 'Liu');
let xcat = buildName('Xcat');
複製程式碼

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

function buildName(firstName?: string, lastName: string) {
  if (firstName) {
    return firstName + ' ' + lastName;
  } else {
    return lastName;
  }
}
let xcatliu = buildName('Xcat', 'Liu');
let xcat = buildName(undefined, 'Xcat');

// index.ts(1,40): error TS1016: A required parameter cannot follow an optional parameter.
複製程式碼

引數預設值

在 ES6 中,我們允許給函式的引數新增預設值,TypeScript 會將新增了預設值的引數識別為可選引數:

function buildName(firstName: string, lastName: string = 'Liu') {
  return firstName + ' ' + lastName;
}
let xcatliu = buildName('Xcat', 'Liu');
let xcat = buildName('Xcat');
複製程式碼

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

function buildName(firstName: string = 'Xcat', lastName: string) {
  return firstName + ' ' + lastName;
}
let xcatliu = buildName('Xcat', 'Liu');
let xcat = buildName(undefined, 'Xcat');
複製程式碼

剩餘引數

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

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

let a = [];
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);
複製程式碼

型別斷言

型別斷言(Type Assertion)可以用來繞過編譯器的型別推斷,手動指定一個值的型別(即程式設計師對編譯器斷言)。

<型別>值

// 或

值 as 型別
複製程式碼

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

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

// index.ts(2,20): error TS2339: Property 'length' does not exist on type 'string | number'.
//   Property 'length' does not exist on type 'number'.
複製程式碼

而有時候,我們確實需要在還不確定型別的時候就訪問其中一個型別的屬性或方法,此時可以使用型別斷言,將 something 斷言成 string

function getLength(something: string | number): number {
  if ((<string>something).length) {
    return (<string>something).length;
  } else {
    return something.toString().length;
  }
}
複製程式碼

相關文章