天天看點

【TS】566- 一文讀懂 TS 中 Object, object, {} 類型之間的差別

TypeScript 2.2 引入了被稱為 ​

​object​

​​ 類型的新類型,它用于表示非原始類型。在 JavaScript 中以下類型被視為原始類型:​

​string​

​​、​

​boolean​

​​、​

​number​

​​、​

​bigint​

​​、​

​symbol​

​​、​

​null​

​​ 和 ​

​undefined​

​。

所有其他類型均被視為非基本類型。新的 ​

​object​

​ 類型表示如下:

// All primitive types
type Primitive = string 
 | boolean | number 
 | bigint | symbol 
 | null | undefined;

// All non-primitive types
type NonPrimitive = object;      

讓我們看看 ​

​object​

​ 類型,如何讓我們編寫更精确的類型聲明。

一、使用 object 類型進行類型聲明

随着 TypeScript 2.2 的釋出,标準庫的類型聲明已經更新,以使用新的對象類型。例如,​

​Object.create()​

​​ 和​

​Object.setPrototypeOf()​

​​ 方法,現在需要為它們的原型參數指定 ​

​object | null​

​ 類型:

// node_modules/typescript/lib/lib.es5.d.ts
interface ObjectConstructor {
  create(o: object | null): any;
  setPrototypeOf(o: any, proto: object | null): any;
  // ...
}      

将原始類型作為原型傳遞給 ​

​Object.setPrototypeOf()​

​​ 或 ​

​Object.create()​

​ 将導緻在運作時抛出類型錯誤。TypeScript 現在能夠捕獲這些錯誤,并在編譯時提示相應的錯誤:

const proto = {};

Object.create(proto);     // OK
Object.create(null);      // OK
Object.create(undefined); // Error
Object.create(1337);      // Error
Object.create(true);      // Error
Object.create("oops");    // Error      

​object​

​ 類型的另一個用例是作為 ES2015 的一部分引入的 WeakMap 資料結構。它的鍵必須是對象,不能是原始值。這個要求現在反映在類型定義中:

interface WeakMap<K extends object, V> {
  delete(key: K): boolean;
  get(key: K): V | undefined;
  has(key: K): boolean;
  set(key: K, value: V): this;
}      

二、Object vs object vs {}

也許令人困惑的是,TypeScript 定義了幾個類型,它們有相似的名字,但是代表不同的概念:

  • ​object​

  • ​Object​

  • ​{}​

我們已經看到了上面的新對象類型。現在讓我們讨論 ​

​Object​

​​ 和 ​

​{}​

​ 表示什麼。

2.1 Object 類型

TypeScript 定義了另一個與新的 ​

​object​

​​ 類型幾乎同名的類型,那就是 ​

​Object​

​ 類型。該類型是所有 Object 類的執行個體的類型。它由以下兩個接口來定義:

  • Object 接口定義了 Object.prototype 原型對象上的屬性;
  • ObjectConstructor 接口定義了 Object 類的屬性。

下面我們來看一下上述兩個接口的相關定義:

1、​

​Object​

​ 接口定義

// node_modules/typescript/lib/lib.es5.d.ts

interface Object {
  constructor: Function;
  toString(): string;
  toLocaleString(): string;
  valueOf(): Object;
  hasOwnProperty(v: PropertyKey): boolean;
  isPrototypeOf(v: Object): boolean;
  propertyIsEnumerable(v: PropertyKey): boolean;
}      

2、​

​ObjectConstructor​

​ 接口定義

// node_modules/typescript/lib/lib.es5.d.ts

interface ObjectConstructor {
  /** Invocation via `new` */
  new(value?: any): Object;
  /** Invocation via function calls */
  (value?: any): any;

  readonly prototype: Object;

  getPrototypeOf(o: any): any;

  // ···
}

declare var Object: ObjectConstructor;      

Object 類的所有執行個體都繼承了 Object 接口中的所有屬性。我們可以看到,如果我們建立一個傳回其參數的函數:

傳入一個 Object 對象的執行個體,它總是會滿足該函數的傳回類型 —— 即要求傳回值包含一個 toString() 方法。

// Object: Provides functionality common to all JavaScript objects.
function f(x: Object): { toString(): string } {
  return x; // OK
}      

而 ​

​object​

​ 類型,它用于表示非原始類型(undefined, null, boolean, number, bigint, string, symbol)。使用這種類型,我們不能通路值的任何屬性。

2.2 Object vs object

有趣的是,類型 ​

​Object​

​ 包括原始值:

function func1(x: Object) { }
func1('semlinker'); // OK      

為什麼?​

​Object.prototype​

​ 的屬性也可以通過原始值通路:

> 'semlinker'.hasOwnProperty === Object.prototype.hasOwnProperty
true      
感興趣的讀者,可以自行了解一下 “JavaScript 裝箱和拆箱” 的相關内容。

相反,​

​object​

​ 類型不包括原始值:

function func2(x: object) { }

// Argument of type '"semlinker"' 
// is not assignable to parameter of type 'object'.(2345)
func2('semlinker'); // Error      

需要注意的是,當對 Object 類型的變量進行指派時,如果值對象屬性名與 Object 接口中的屬性沖突,則 TypeScript 編譯器會提示相應的錯誤:

// Type '() => number' is not assignable to type 
// '() => string'.
// Type 'number' is not assignable to type 'string'.
const obj1: Object = { 
   toString() { return 123 } // Error
};       

而對于 object 類型來說,TypeScript 編譯器不會提示任何錯誤:

const obj2: object = { 
  toString() { return 123 } 
};      

另外在處理 object 類型和字元串索引對象類型的指派操作時,也要特别注意。比如:

let strictTypeHeaders: { [key: string]: string } = {};
let header: object = {};
header = strictTypeHeaders; // OK
// Type 'object' is not assignable to type '{ [key: string]: string; }'.
strictTypeHeaders = header; // Error      

在上述例子中,最後一行會出現編譯錯誤,這是因為 ​

​{ [key: string]: string }​

​​ 類型相比 ​

​object​

​​ 類型更加精确。而 ​

​header = strictTypeHeaders;​

​​ 這一行卻沒有提示任何錯誤,是因為這兩種類型都是非基本類型,​

​object​

​​ 類型比 ​

​{ [key: string]: string }​

​ 類型更加通用。

2.3 空類型 {}

還有另一種類型與之非常相似,即空類型:​

​{}​

​。它描述了一個沒有成員的對象。當你試圖通路這樣一個對象的任意屬性時,TypeScript 會産生一個編譯時錯誤:

// Type {}
const obj = {};

// Error: Property 'prop' does not exist on type '{}'.
obj.prop = "semlinker";      

但是,你仍然可以使用在 Object 類型上定義的所有屬性和方法,這些屬性和方法可通過 JavaScript 的原型鍊隐式地使用:

// Type {}
const obj = {};

// "[object Object]"
obj.toString();      

在 JavaScript 中建立一個表示二維坐标點的對象很簡單:

const pt = {}; 
pt.x = 3; 
pt.y = 4;      

然而以上代碼在 TypeScript 中,每個指派語句都會産生錯誤:

const pt = {}; // (A)
// Property 'x' does not exist on type '{}'
pt.x = 3; // Error
// Property 'y' does not exist on type '{}'
pt.y = 4; // Error      

這是因為第 A 行中的 pt 類型是根據它的值 {} 推斷出來的,你隻可以對已知的屬性指派。這個問題怎麼解決呢?有些讀者可能會先想到接口,比如這樣子:

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

// Type '{}' is missing the following 
// properties from type 'Point': x, y(2739)
const pt: Point = {}; // Error
pt.x = 3;
pt.y = 4;      

很可惜對于以上的方案,TypeScript 編譯器仍會提示錯誤。那麼這個問題該如何解決呢?其實我們可以直接通過對象字面量進行指派:

const pt = { 
  x: 3,
  y: 4, 
}; // OK      

而如果你需要一步一步地建立對象,你可以使用類型斷言(as)來消除 TypeScript 的類型檢查:

const pt = {} as Point; 
pt.x = 3;
pt.y = 4; // OK      

但是更好的方法是聲明變量的類型并一次性建構對象:

const pt: Point = { 
  x: 3,
  y: 4, 
};      

另外在使用 ​

​Object.assign​

​ 方法合并多個對象的時候,你可能也會遇到以下問題:

const pt = { x: 666, y: 888 };
const id = { name: "semlinker" };
const namedPoint = {};
Object.assign(namedPoint, pt, id);

// Property 'name' does not exist on type '{}'.(2339)
namedPoint.name; // Error      

這時候你可以使用對象展開運算符 ​

​...​

​ 來解決上述問題:

const pt = { x: 666, y: 888 };
const id = { name: "semlinker" };
const namedPoint = {...pt, ...id}

//(property) name: string
namedPoint.name // Ok      

三、對象字面量類型 vs 接口類型

我們除了可以通過 Object 和 object 類型來描述對象之外,也可以通過對象的屬性來描述對象:

// Object literal type
let obj3: { prop: boolean };

// Interface
interface ObjectType {
  prop: boolean;
}

let obj4: ObjectType;      

在 TypeScript 中有兩種定義對象類型的方法,它們非常相似:

// Object literal type
type ObjType1 = {
  a: boolean,
  b: number;
  c: string,
};

// Interface
interface ObjType2 {
  a: boolean,
  b: number;
  c: string,
}      

在以上代碼中,我們使用分号或逗号作為分隔符。尾随分隔符是允許的,也是可選的。好的,那麼現在問題來了,對象字面量類型和接口類型之間有什麼差別呢?下面我從以下幾個方面來分析一下它們之間的差別:

3.1 内聯

對象字面量類型可以内聯,而接口不能:

// Inlined object literal type:
function f1(x: { prop: number }) {}

function f2(x: ObjectInterface) {} // referenced interface
interface ObjectInterface {
  prop: number;
}      

3.2 名稱重複

含有重複名稱的類型别名是非法的:

// @ts-ignore: Duplicate identifier 'PersonAlias'. (2300)
type PersonAlias = {first: string};

// @ts-ignore: Duplicate identifier 'PersonAlias'. (2300)
type PersonAlias = {last: string};      
TypeScript 2.6 支援在 .ts 檔案中通過在報錯一行上方使用 ​

​// @ts-ignore​

​ 來忽略錯誤。

​// @ts-ignore​

​​ 注釋會忽略下一行中産生的所有錯誤。建議實踐中在 ​

​@ts-ignore​

​之後添加相關提示,解釋忽略了什麼錯誤。

請注意,這個注釋僅會隐藏報錯,并且我們建議你少使用這一注釋。

相反,含有重複名稱的接口将會被合并:

interface PersonInterface {
  first: string;
}

interface PersonInterface {
  last: string;
}

const sem: PersonInterface = {
  first: 'Jiabao',
  last: 'Huang',
};      

3.3 映射類型

對于映射類型(A行),我們需要使用對象字面量類型:

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

type PointCopy1 = {
  [Key in keyof Point]: Point[Key]; // (A)
};

// Syntax error:
// interface PointCopy2 {
//   [Key in keyof Point]: Point[Key];
// };      

3.4 多态 this 類型

多态 this 類型僅适用于接口:

interface AddsStrings {
  add(str: string): this;
};

class StringBuilder implements AddsStrings {
  result = '';
  add(str: string) {
    this.result += str;
    return this;
  }
}      

四、總結

相信很多剛接觸 TypeScript 的讀者,看到 Object、object 和 {} 這幾種類型時,也會感到疑惑。因為不知道它們之間的有什麼差別,什麼時候使用?為了讓讀者能更直覺的了解到它們之間的差別,最後我們來做個總結:

4.1 object 類型

object 類型是:TypeScript 2.2 引入的新類型,它用于表示非原始類型。

// node_modules/typescript/lib/lib.es5.d.ts
interface ObjectConstructor {
  create(o: object | null): any;
  // ...
}

const proto = {};

Object.create(proto);     // OK
Object.create(null);      // OK
Object.create(undefined); // Error
Object.create(1337);      // Error
Object.create(true);      // Error
Object.create("oops");    // Error      

4.2 Object 類型

Object 類型:它是所有 Object 類的執行個體的類型。它由以下兩個接口來定義:

它由以下兩個接口來定義:

  • Object 接口定義了 Object.prototype 原型對象上的屬性;
// node_modules/typescript/lib/lib.es5.d.ts

interface Object {
  constructor: Function;
  toString(): string;
  toLocaleString(): string;
  valueOf(): Object;
  hasOwnProperty(v: PropertyKey): boolean;
  isPrototypeOf(v: Object): boolean;
  propertyIsEnumerable(v: PropertyKey): boolean;
}      
  • ObjectConstructor 接口定義了 Object 類的屬性。
// node_modules/typescript/lib/lib.es5.d.ts

interface ObjectConstructor {
  /** Invocation via `new` */
  new(value?: any): Object;
  /** Invocation via function calls */
  (value?: any): any;

  readonly prototype: Object;

  getPrototypeOf(o: any): any;

  // ···
}

declare var Object: ObjectConstructor;      

Object 類的所有執行個體都繼承了 Object 接口中的所有屬性。

4.3 {} 類型

// Type {}
const obj = {};

// Error: Property 'prop' does not exist on type '{}'.
obj.prop = "semlinker";      

五、參考資源

  • the-object-type-in-typescript
  • typing-objects-typescript
  • difference-between-object-and-in-typescript

繼續閱讀