likes
comments
collection
share

这一次,彻底掌握TypeScript(一)基本类型&语法

作者站长头像
站长
· 阅读数 19

TypeScript 是一种由微软开发的自由和开源的编程语言,是 JavaScript 的一个超集,其为 JavaScript 引入了可选的静态类型,在最近几年其热度不断上升已然成为了大型企业项目构建不可或缺的选择。本文是 TypeScript 系列博客的第一篇。

TypeScript 相比于 JavaScript 它的特点主要有以下三点:

  • 类型检查:TypwScript 为 JavaScript 引入静态类型,使我们可以在编译阶段发现问题而不是运行时
  • 语言扩展:TypeScript 不仅仅包括了 ES6 及未来提案中的一些特性,还从其他语言借鉴了一些特性,比如接口和抽象类
  • 工具属性:TypeScript 可以编译生成 JavaScript 运行在浏览器及不同操作系统上,无其他运行时开销

其中最核心的特点当然是类型检查,那么为什么我们需要 TypeScript 帮我们引入静态类型呢?

静态类型语言可以在编译阶段确定所有变量的类型,而动态类型语言只能在执行阶段确定所有变量的类型,我们以相同功能的 Javascript 和 C++ 代码为例:

// javascript
class C {
  constructor(x, y) {
    this.x = x;
    this.y = y;
  }
}
function add(a, b) {
  return a.x + a.y + b.x + b.y
}
// C++ 
class C {
  public:
    int x;
    int y;
}
int add(C a, C b) {
  return a.x + a.y + b.x + b.y
 }

在上述示例中 a、b 都是类 C 的实例对象,add 函数返回 a、b 对象所有属性的和,对于 JavaScript 而言,属性结构图如下图所示: 这一次,彻底掌握TypeScript(一)基本类型&语法

  • 在程序运行时动态计算属性偏移量
  • 需要额外的空间存储属性名
  • 所有对象的属性偏移量量信息各存一份

而对于C++而言:

这一次,彻底掌握TypeScript(一)基本类型&语法

  • 编译阶段确定属性偏移量
  • 偏移量访问代替属性名访问
  • 属性偏移量信息共享

由此可以看到动态类型语言无论在时间还是空间上都有比较多的性能损耗,虽然实际上 V8 为了提升 JavaScript 运行时的性能做了很多优化,但是静态类型的语言相比于动态类型往往具有更优秀的运行时性能。

除此之外 TypeScript 更重要的价值在于将静态类型的编程思维引入了 JavaScript,让我们可以在编译阶段以一种完全不同的视角去看待我们的代码,更早的发现代码中的错误也可以让我们进一步优化我们的代码让其更具可读性与扩展性。

作为这个系列的第一章本文主要讲解 TypeScript 中的基本类型与语法。

一、boolean 类型

let bool1: boolean = true
let bool2 = false // boolean
const bool3 = false // false

二、number 类型

let num1: number = 123
let num2 = 234 // number
const num3 = 345 // 345

三、string类型

let str1: string = 'abc'
let str2 = 'bcd' // string
const str3 = 'cde' // 'cde'

总结:我们可以通过直接声明或让 TypeScript 推导等多种方式声明 boolean / number / string 类型,需要注意的是通过 const 关键字声明会将类型收敛为具体的属性值

四、symbol类型

symbol 类型主要是针对 JavaScript 所有 Symbol 类型数据变量,它还包含一个子类型:unique symbol,不过unique symbol 与声明紧密相关,只允许在 const 声明中引用这个确切的符号。

let s1 = Symbol('a'); // symbol
let s2: symbol = Symbol('a')
const s3 = Symbol('a') // typeof s3
const s4: unique symbol = Symbol('a') // typeof s4
let s5: unique symbol = Symbol('a') // Error: A variable whose type is a 'unique symbol' type must be 'const'.

这儿需要注意的是使用 const 声明的变量,TypeScript 会推导为 unique symbol 类型,在代码编辑器中显示为 type of yourVariablename,而不是 unique symbol

五、any 类型

在 TypeScript 中,任何类型都可以被归为 any 类型。这让 any 类型成为了类型系统的顶级类型(也被称作全局超级类型)。

let notSure: any = 666;
notSure = "semlinker";
notSure = false;

any 类型本质上是类型系统的一个逃逸舱。作为开发者,这给了我们很大的自由:TypeScript 允许我们对 any 类型的值执行任何操作,而无需事先执行任何形式的检查。比如:

let value: any;

value.foo.bar; // OK
value.trim(); // OK
value(); // OK
new value(); // OK
value[0][1]; // OK

在许多场景下,这太宽松了。使用 any 类型,可以很容易地编写类型正确但在运行时有问题的代码。如果我们使用 any 类型,就无法使用 TypeScript 提供的大量的保护机制。

如果想让 TypeScript 在遇到隐式 any 类型报错我们可以在 tsconfig.json 中启用 noImplicitAny 配置,为了解决 any 带来的问题,TypeScript 3.0 引入了 unknown 类型。

六、unknown 类型

就像所有类型都可以赋值给 any,所有类型也都可以赋值给 unknown。这使得 unknown 成为 TypeScript 类型系统的另一种顶级类型。下面我们来看一下 unknown 类型的使用示例:

let value: unknown;

value = true; // OK
value = 42; // OK
value = "Hello World"; // OK
value = []; // OK
value = {}; // OK
value = Math.random; // OK
value = null; // OK
value = undefined; // OK
value = new TypeError(); // OK
value = Symbol("type"); // OK

对 value 变量的所有赋值都被认为是类型正确的。但是,当我们尝试将类型为 unknown 的值赋值给其他类型的变量时会发生什么?

let value: unknown;

let value1: unknown = value; // OK
let value2: any = value; // OK
let value3: boolean = value; // Error
let value4: number = value; // Error
let value5: string = value; // Error
let value6: object = value; // Error
let value7: any[] = value; // Error
let value8: Function = value; // Error

unknown 类型只能被赋值给 any 类型和 unknown 类型本身。直观地说,这是有道理的:只有能够保存任意类型值的容器才能保存 unknown 类型的值。毕竟我们不知道变量 value 中存储了什么类型的值。 现在让我们看看当我们尝试对类型为 unknown 的值执行操作时会发生什么。以下是我们在之前 any 章节看过的相同操作:

let value: unknown;

value.foo.bar; // Error
value.trim(); // Error
value(); // Error
new value(); // Error
value[0][1]; // Error

将 value 变量类型设置为 unknown 后,这些操作都不再被认为是类型正确的。通过将 any 类型改变为 unknown 类型,我们已将允许所有更改的默认设置,更改为禁止任何更改,因此我们执行操作时不能假定 unknown 类型的值为某种特定类型,必须先向 typeScript 证明一个值确实是某个类型,举个🌰:

let a: unknown = 30
let c = a + 30 // Error: object is of type
if (typeof a === 'number') {
  let d = a + 10
}

七、数组类型

let list: number[] = [1, 2, 3];
let list: Array<number> = [1, 2, 3]; // Array<number>泛型语法

TypeScript 支持两种注解数组类型的句法:T[]Array<T>,两者的作用和性能无异,可以根据个人喜好进行选择。

一般情况下数组应该保持同质,TypeScript 在类型推断时也会尽可能收窄,举个🌰:

let a = [1, 2, 3] // number[]
let b = [1, 'a'] // (string | number)[]
const c = [2, 'b'] // (string | number)[]
let d = [] // any[]
d.push(1) // number[]
d.push('red') // (string | number)[]

我们可以注意到使用 const 声明数组并不会导致 TypeScript 推导出更窄的数据类型,事实上所有的引用类型都不会被 const 声明收窄,这也与 const 声明的引用类型的属性还可以再修改有关。

在初始化空数组时,TypeScript 不知道数组中的元素类型,推导出类型为 any,向数组中添加元素后,TypeScript 开始拼凑数组的类型,当数组离开定义时所在的作用域后,TypeScript 将最终确定一个类型,不再扩张。举个🌰:

function buildArray() {
  let a = [] // any[]
  a.push(1) // number[]
  a.push('y') // (string | number)[]
  return a
}
let myArray = buildArray() // (string | number)[]
myArray.push(true) // Argument of type 'boolean' is not assignable to parameter of type 'string | number'.

八、元组类型

众所周知,数组一般由同种类型的值组成,但有时我们需要在单个变量中存储不同类型的值,这时候我们就可以使用元组。在 JavaScript 中是没有元组的,元组是 TypeScript 中特有的类型,是数组的子类型,其工作方式类似于数组。 元组可用于定义具有有限数量的未命名属性的类型,每个属性都有一个关联的类型。使用元组时,必须提供每个属性的值,举个🌰:

let tupleType: [string, boolean];
tupleType = ["semlinker", true];

console.log(tupleType[0]) // 'semlinker'

在上述示例中我们定义了一个名为 tupleType 的变量,它的类型是一个类型数组 [string, boolean],然后我们按照正确的类型依次初始化 tupleType 变量,并通过下标来访问其中元素,需要注意的是初始化元组时不仅仅需要保证每个属性类型的一致,同时必须提供每个属性的值,否则都会报错。

元组也支持剩余元素,即为元组定义最小长度,举个🌰:

// 字符串列表,至少一个元素
let friends: [string, ...string[]] = ['一鸣', '化腾', '小云']
// 元素类型不同的列表
let list: [number, boolean, ...string[]] = [1, false, 'a']

常规数组是可变的,这也是多数时候我们想要的行为,不过有时我们希望数组不可变,修改之后得到新的数组而原数组没有变化,想要创建只读数组有如下方法注解类型:

let a: readonly number[] = [1, 2, 3]
let b: Readonly<number[]> = [1, 2, 3]
let c: ReadonlyArray<number> = [1, 2, 3]

let d: readonly [number, string] = [1, 'a']
let e: Readonly<[number, string]> = [1, 'a']

上面示例中我们可以看出若想创建只读数组,要显式注解类型,若想更改只读数组,只能使用非变型方法,比如 .concat.slice,不能使用可变型方法,例如 .push.splice

九、枚举类型

枚举类型:一组有名字的常量集合,使用枚举可以清晰地表达意图或创建一组有区别的用例。 TypeScript 支持数字的和基于字符串的枚举。

1、数字枚举

enum Direction {
  NORTH, // 0
  SOUTH, // 1
  EAST, // 2
  WEST, // 3
}
let dir: Direction = Direction.NORTH;

默认情况下,NORTH 的初始值为0,其余的成员会从1开始自动增长。换句话说,Direction.SOUTH 的值为1,Direction.EAST 的值为2,Direction.WEST 的值为3。 以上的枚举示例经编译后,对应的 ES5 代码如下:

"use strict";
var Direction;
(function (Direction) {
  Direction[(Direction["NORTH"] = 0)] = "NORTH";
  Direction[(Direction["SOUTH"] = 1)] = "SOUTH";
  Direction[(Direction["EAST"] = 2)] = "EAST";
  Direction[(Direction["WEST"] = 3)] = "WEST";
})
(Direction || (Direction = {}));
var dir = Direction.NORTH;

从上面代码我们可以看到 TypeScript 通过反向映射实现了数字枚举,但是其他枚举却又有所不同。

2、字符串枚举

enum Direction {
  NORTH = "NORTH",
  SOUTH = "SOUTH",
  EAST = "EAST",
  WEST = "WEST",
}

对应的 ES5 代码如下:

"use strict";
var Direction;
(function (Direction) {
  Direction["NORTH"] = "NORTH";
  Direction["SOUTH"] = "SOUTH";
  Direction["EAST"] = "EAST";
  Direction["WEST"] = "WEST";
})
(Direction || (Direction = {}));

从上面示例中我们可以发现字符串枚举没有实现反向映射。

3、异构枚举

异构枚举的成员值是数字和字符串的混合:

enum Enum {
  A,
  B,
  C = "C",
  D = "D",
  E = 8,
  F,
}

对应的ES5代码如下:

"use strict";
var Enum;
(function (Enum) {
  Enum[Enum["A"] = 0] = "A";
  Enum[Enum["B"] = 1] = "B";
  Enum["C"] = "C";
  Enum["D"] = "D";
  Enum[Enum["E"] = 8] = "E";
  Enum[Enum["F"] = 9] = "F";
})
(Enum || (Enum = {}));

从示例我们可以发现异构枚举中的数字成员实现了反向映射,而字符串成员没有,但是异构枚举容易造成混淆,不推荐使用。

对于数字枚举和异构枚举,TypeScript 既允许通过值访问枚举,也允许通过键访问,不过这样极易导致问题,举个🌰:

// 数字枚举
enum Direction1 {
  NORTH, // 0
  SOUTH, // 1
  EAST, // 2
  WEST, // 3
}
console.log(Direction1[0]) // 'NORTH'
console.log(Direction1[10]) // undefined

// 字符串枚举
enum Direction2 {
  NORTH = "NORTH",
  SOUTH = "SOUTH",
  EAST = "EAST",
  WEST = "WEST",
}
console.log(Direction2[0]) // Error: Property '0' does not exist on type 'typeof Direction'.(7053)

// 异构枚举
enum Direction3 {
  NORTH, // 0
  SOUTH, // 1
  EAST = "EAST",
  WEST = "WEST",
}
console.log(Direction3[0]) // 'NORTH'
console.log(Direction3[2]) // undefined
console.log(Direction3[10]) // undefined

其实上述实例中部分枚举值并不存在例如 Direction1[10] ,但是 TypeScript 并没有阻止这种操作,为了避免这种不安全的访问操作,我们可以使用常量枚举。

4、常量枚举

除了数字枚举和字符串枚举之外,还有一种特殊的枚举——常量枚举。它是使用 const 关键字修饰的枚举,常量枚举会使用内联语法,不会为枚举类型编译生成任何 JavaScript,举个🌰:

const enum Direction {
  NORTH,
  SOUTH,
  EAST,
  WEST,
}

let dir: Direction = Direction.NORTH;

对应的ES5代码如下:

"use strict";
var dir = 0 /* NORTH */;

常量枚举对于上述不安全操作有了更好的处理:

const enum Direction {
  NORTH,
  SOUTH,
  EAST,
  WEST,
}
console.log(Direction.MIDDLE) // Error: Property 'MIDDLE' does not exist on type 'typeof Direction'.(2339) 
console.log(Direction[0]) // Error: A const enum member can only be accessed using a string literal. 
console.log(Direction[10]) // Error: A const enum member can only be accessed using a string literal.

5、枚举成员

枚举成员的值具有如下特性:

  • 只读:枚举类型初始化以后不支持属性的修改,即枚举类型成员的属性都是只读属性
  • 类型:枚举类型成员的值包括两种类型:常量类型(const number)计算类型(computer number) ,常量类型包括没有初始值、引用已有枚举属性值和常量表达式三类,常量类型会在编译阶段编译出结果,以常量的形式出现在运行时环境,计算类型主要是一些非常量的表达式,这些表达式的值不会在编译阶段被计算而是保留到执行时阶段。

我们举个🌰:

enum Char {
  // const number
  a,
  b = Char.a,
  c = 1 + 3,
  // computer number
  d = Math.random(),
  e = '1234'.length
}

对应的ES5的代码如下:

var Char;
(function (Char) {
  // const number
  Char[Char["a"] = 0] = "a";
  Char[Char["b"] = 0] = "b";
  Char[Char["c"] = 4] = "c";
  // computer number
  Char[Char["d"] = Math.random()] = "d";
  Char[Char["e"] = '1234'.length] = "e";
})
(Char || (Char = {}));

十、void 类型

在 JavaScript 中 void 是关键字,最关键的用途是获取 undefined:

void 0; // undefined

我们通过它可以有效避免 undefined 常量被重新赋值的情形,但在 TypeScript 中 void 类型表示没有任何返回:

function func(): void {
  console.log("This is a function");
}

需要注意的是,声明一个 void 类型的变量没有什么作用,因为在严格模式下,它的值只能为 undefined:

let unusable: void = undefined;

十一、null 和 undefined类型

TypeScript 里,undefined 和 null 两者有各自的类型分别为 undefined 和 null。

let u: undefined = undefined;
let n: null = null;

需要注意的是在 TypeScript 规范中 undefined 和 null 是所有类型的子类型,所以当我们将 tsconfig.json 中的 strictNullChecks 配置设为 false 时我们可以将 null 和 undefined 赋值给其他类型的值,举个🌰:

let num: number = 123;
num = null;
num = undefined;

十二、never类型

never 类型表示的是那些永不存在的值的类型。 例如,never 类型是那些总是会抛出异常或根本就不会有返回值的函数表达式或箭头函数表达式的返回值类型。

// 返回never的函数必须存在无法达到的终点
function error(message: string): never {
  throw new Error(message);
}

function infiniteLoop(): never {
  while (true) {}
}

在 TypeScript 中,可以利用 never 类型的特性来实现全面性检查,具体示例如下:

type Foo = string | number;
function controlFlowAnalysisWithNever(foo: Foo) {
  if (typeof foo === "string") {
    // 这里 foo 被收窄为 string 类型
  } else if (typeof foo === "number") {
    // 这里 foo 被收窄为 number 类型
  } else {
    // foo 在这里是 never const check: never = foo;
  }
}

注意在 else 分支里面,我们把收窄为 never 的 foo 赋值给一个显式声明的 never 变量。如果一切逻辑正确,那么这里应该能够编译通过。但是假如后来有一天你的同事修改了 Foo 的类型:

type Foo = string | number | boolean;

然而他忘记同时修改 controlFlowAnalysisWithNever 方法中的控制流程,这时候 else 分支的 foo 类型会被收窄为 boolean 类型,导致无法赋值给 never 类型,这时就会产生一个编译错误。通过这个方式,我们可以确保 controlFlowAnalysisWithNever 方法总是穷尽了 Foo 的所有可能类型。

通过这个示例,我们可以得出一个结论:使用 never 避免出现新增了联合类型没有对应的实现,目的就是写出类型绝对安全的代码。

十三、bigint类型

BigInt 是 ECMAScript 的一项提案,它在理论上允许我们建模任意大小的整数。但是 TypeScript 3.2 引入了一个新的原始类型 bigint,允许我们为 BigInit 数据类型进行类型检查,并支持在目标为 esnext 时输出 BigInit 字面量,我们可以通过调用 BigInt() 函数或书写 BigInt 字面量(或在整型数字字面量末尾添加 n)来获取 bigint:

let foo: bigint = BigInt(100); // the BigInt function
let bar: bigint = 100n;        // a BigInt literal
// *Slaps roof of fibonacci function*
// This bad boy returns ints that can get *so* big!
function fibonacci(n: bigint) {
  let result = 1n;
  for (let last = 0n, i = 0n; i < n; i++) {
    const current = result;
    result += last;
    last = current;
  }
  return result;
}
fibonacci(10000n)

需要注意的是 bigint 和 number 之间无法混用,是完全不同的东西:

let foo: number;
let bar: bigint;
foo = bar; // error: Type 'bigint' is not assignable to type 'number'. 
bar = foo; // error: Type 'number' is not assignable to type 'bigint'.

还有一点要注意的是,对 bigint 使用 typeof 操作符返回一个新的字符串:“bigint”。因此,TypeScript 能够正确地使用 typeof 细化类型,举个🌰:

function whatKindOfNumberIsIt(x: number | bigint) {
  if (typeof x === "bigint") {
    console.log("'x' is a bigint!");
  } else {
    console.log("'x' is a floating-point number");
  }
}

十四、object、Object 和 {} 类型

1、object类型

TypeScript 2.2 引入了被称为 object 类型的新类型,它用于表示非原始类型,在 JavaScript 中以下类型被视为原始类型:string、boolean、number、bigint、symbol、null 和 undefined。

它的引入主要是因为随着 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: Argument of type 'undefined' is not assignable to parameter of type 'object | null'.
Object.create(1337);      // Error: Argument of type 'number' is not assignable to parameter of type 'object'.
Object.create(true);      // Error: Argument of type 'boolean' is not assignable to parameter of type 'object'.
Object.create("oops");    // Error: Argument of type 'string' is not assignable to parameter of type 'object'.

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;
}

2、Object类型

TypeScript 定义了另一个与新的 object 类型几乎同名的类型,那就是 Object 类型。该类型是所有 Object 类的实例的类型,实际上 Object 类由以下两个接口来定义:

  • Object 接口定义了 Object.prototype 原型对象上的属性
  • ObjectConstructor 接口定义了 Object 类的属性

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;
}

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 接口中的所有属性。我们可以看到,如果我们创建一个返回其参数的函数:

function f(x: Object): { toString(): string } {
  return x; // OK
}

当我们传入一个 Object 对象的实例时,它总是会满足该函数的返回类型 —— 即要求返回对象包含一个 toString() 方法。

有趣的是,类型 Object 包括原始值:

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

实际上这是因为 Object.prototype 的属性也可以通过原始值访问:

'semlinker'.hasOwnProperty === Object.prototype.hasOwnProperty // true

相反,object 类型不包括原始值:

function func2(x: object) { }

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

需要注意的是,当对 Object 类型的变量进行赋值时,如果值对象属性名与 Object 接口中的属性冲突,则 TypeScript 编译器会提示相应的错误:

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

而对于 object 类型来说,TypeScript 编译器不会提示任何错误:

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

另外在处理 object 类型和字符串索引对象类型的赋值操作时,也要特别注意。比如:

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

在上述例子中,最后一行会出现编译错误,这是因为 { [key: string]: string } 类型相比 object 类型更加精确。而 header = strictTypeHeaders; 这一行却没有提示任何错误,是因为这两种类型都是非基本类型,object 类型比 { [key: string]: string } 类型更加通用。

3、{}类型

还有另一种类型与之非常相似,即空对象类型:{}。它描述了一个没有成员的对象。当你试图访问这样一个对象的任意属性时,TypeScript 会产生一个编译时错误:

// Type {}
const obj = {};

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

但是,你仍然可以使用在 Object 类型上定义的所有属性和方法,这些属性和方法可通过 JavaScript 的原型链隐式地使用:

// Type {}
const obj = {};

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

并且除 null 和 undefined 之外的任何类型都可以赋值给空对象类型,举个🌰:

let danger = {}
danger = {}
danger = { x: 1 }
danger = []
danger = 123
danger = 'danger'

这极易造成误解,因此我们应该尽可能避免使用空对象类型,我们用以下示例说明空对象类型在日常写法中带来的快(tong)乐(ku):

在 JavaScript 中创建一个表示二维坐标点的对象很简单:

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

然而以上代码在 TypeScript 中,每个赋值语句都会产生错误:

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

这是因为第1行中的 pt 类型是根据它的值 {} 推断出来的,你只可以对已知的属性赋值。这个问题怎么解决呢?我们可能会先想到接口,比如这样子:

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

const pt: Point = {}; // Error: Type '{}' is missing the following properties from type 'Point': x, y(2739)
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);

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

这时候你可以使用对象展开运算符 来解决上述问题:

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

namedPoint.name // "semlinker"

参考资料

极客时间《TypeScript开发实战》专栏

《深入理解TypeScript》

《TypeScript编程》

一文读懂 TS 中 Object, object, {} 类型之间的区别

一份不可多得的 TS 学习指南(1.8W字)

Typescript使用手册