TypeScript 加强

# 类型兼容性

# 结构类型 vs 名义类型

在 Ts 里,有两种兼容性:子类型和赋值。它们的不同点在于,赋值扩展了子类型兼容性,增加了一些规则,允许和 any 来回赋值,以及 enum 和对应数字值之间的来回赋值。

语言里的不同地方分别使用了它们之中的机制。实际上,类型兼容性是由赋值兼容性来控制的,即使在 implements 和 extends 语句也不例外。

Ts 里的类型兼容性是基于结构类型的。结构类型是一种只使用其成员来描述类型的方式。它是基于类型的组成结构,且不要求明确地声明。

它正好与名义(nominal)类型形成对比。在基于名义类型的类型系统中,数据类型的兼容性或等价性是通过明确声明类型的名称来决定的。

Ts 的类型系统允许某些在编译阶段无法确认其安全性的操作。

# 赋值兼容

interface Named {
  name: string;
}

class Person {
  name: string;
}

let p: Named;
// OK, because of structural typing
p = new Person();
1
2
3
4
5
6
7
8
9
10
11

# 类型兼容性

Ts 结构化类型系统的基本规则是,如果 x 要兼容 y,那么 y 至少具有与 x 相同的属性。

interface Named {
  name: string;
}

let x: Named;
// y's inferred type is { name: string; location: string; }
let y = { name: "Alice", location: "Seattle" };
x = y; // OK
1
2
3
4
5
6
7
8

# 函数参数兼容性

规则同上。如果 x 要兼容 y,那么 y 至少具有与 x 相同的属性。

function greet(n: Named) {
  console.log("Hello, " + n.name);
}
greet(y); // OK
1
2
3
4

# 函数兼容性

判断两个函数的兼容性规则正好与判断类型相反。

let x = (a: number) => 0;
let y = (b: number, s: string) => 0;

y = x; // OK
x = y; // Error
1
2
3
4
5
let items = [1, 2, 3];

// Don't force these extra arguments
items.forEach((item, index, array) => console.log(item));

// Should be OK!,忽略参数是可以的
items.forEach((item) => console.log(item));
1
2
3
4
5
6
7

# 函数返回值类型兼容性

类型系统强制源函数的返回值类型必须是目标函数返回值类型的子类型。

let x = () => ({ name: "Alice" });
let y = () => ({ name: "Alice", location: "Seattle" });

x = y; // OK, 源函数返回值类型是目标函数返回值类型的子类。
y = x; // Error, because x() lacks a location property
1
2
3
4
5

# 函数参数双向协变

相当于面向对象的里氏替换原则。

enum EventType {
  Mouse,
  Keyboard,
}

interface Event {
  timestamp: number;
}
interface MouseEvent extends Event {
  x: number;
  y: number;
}
interface KeyEvent extends Event {
  keyCode: number;
}

function listenEvent(eventType: EventType, handler: (n: Event) => void) {
  /* ... */
}

// Unsound, but useful and common
// 不健全,但常用
listenEvent(EventType.Mouse, (e: MouseEvent) => console.log(e.x + "," + e.y));

// Undesirable alternatives in presence of soundness
// 存在健全性的情况下不需要的替代方案
listenEvent(EventType.Mouse, (e: Event) =>
  console.log((<MouseEvent>e).x + "," + (<MouseEvent>e).y)
);
listenEvent(EventType.Mouse, <(e: Event) => void>(
  ((e: MouseEvent) => console.log(e.x + "," + e.y))
));

// Still disallowed (clear error). Type safety enforced for wholly incompatible types
// 仍然不允许(明显错误)。 针对完全不兼容的类型强制执行类型安全
listenEvent(EventType.Mouse, (e: number) => console.log(e));
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36

# 可选参数及剩余参数

比较函数兼容性的时候,可选参数与必须参数是可互换的。源类型上有额外的可选参数不是错误,目标类型的可选参数在源类型里没有对应的参数也不是错误。

function invokeLater(args: any[], callback: (...args: any[]) => void) {
  /* ... Invoke callback with 'args' ... */
}

// Unsound - invokeLater "might" provide any number of arguments
invokeLater([1, 2], (x, y) => console.log(x + ", " + y));

// Confusing (x and y are actually required) and undiscoverable
invokeLater([1, 2], (x?, y?) => console.log(x + ", " + y));
1
2
3
4
5
6
7
8
9

# 枚举类型与数字类型兼容

枚举与数字相互兼容,不同枚举类型之间是不兼容的。

enum Status {
  Ready,
  Waiting,
}
enum Color {
  Red,
  Blue,
  Green,
}

let status = Status.Ready;
status = Color.Green; // Error
1
2
3
4
5
6
7
8
9
10
11
12

类与对象字面量和接口差不多,但有一点不同,类有静态部分和实例部分的类型。比较两个类类型的对象时,只有实例的成员会被比较。 静态成员和构造函数不在比较的范围内。

类的私有成员和受保护成员会影响兼容性。当检查类实例的兼容时,如果目标类型包含一个私有成员,那么源类型必须包含来自同一个类的这个私有成员。同样地,这条规则也适用于包含受保护成员实例的类型检查。 这允许子类赋值给父类,但是不能赋值给其它有同样类型的类。

class Animal {
  feet: number;
  constructor(name: string, numFeet: number) {}
}

class Size {
  feet: number;
  constructor(numFeet: number) {}
}

let a: Animal;
let s: Size;

a = s; // OK
s = a; // OK
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

# 泛型兼容性

因为 Ts 是结构性的类型系统,类型参数只影响使用其做为类型一部分的结果类型。

interface Empty<T> {}
let x: Empty<number>;
let y: Empty<string>;

x = y; // OK, because y matches structure of x
1
2
3
4
5
interface NotEmpty<T> {
  data: T;
}
let x: NotEmpty<number>;
let y: NotEmpty<string>;

x = y; // Error, because x and y are not compatible
1
2
3
4
5
6
7

对于没指定泛型类型的泛型参数时,会把所有泛型参数当成 any 比较。 然后用结构类型进行比较。

let identity = function <T>(x: T): T {
  // ...
};

let reverse = function <U>(y: U): U {
  // ...
};

identity = reverse; // OK, because (x: any) => any matches (y: any) => any
1
2
3
4
5
6
7
8
9

# 类型推论

# 基础

let x = 3; //number
let x = [0, 1, null]; //number|null
1
2

# 最佳通用类型

//(Rhino | Elephant | Snake)[]  如果是子类可以推断更通用的Animal[]
let zoo = [new Rhino(), new Elephant(), new Snake()];
1
2

# 根据上下文推断

window.onmousedown = function(mouseEvent) {
    console.log(mouseEvent.button);  //<- Error
};

window.onmousedown = function(mouseEvent: any) {
    console.log(mouseEvent.button);  //<- Now, no error is given
};

这个例子里,最佳通用类型有4个候选者:Animal,Rhino,Elephant和Snake。 当然,Animal会被做为最佳通用类型。
function createZoo(): Animal[] {
    return [new Rhino(), new Elephant(), new Snake()];
}
1
2
3
4
5
6
7
8
9
10
11
12

# 高级类型

# 交叉类型(Intersection Types)

交叉类型是将多个类型合并为一个类型。这让我们可以把现有的多种类型叠加到一起成为一种类型,它包含了所需的所有类型的特性。例如, Person & Serializable & Loggable 同时是 Person 和 Serializable 和 Loggable。就是说这个类型的对象同时拥有了这三种类型的成员。

function extend<T, U>(first: T, second: U): T & U {
  let result = <T & U>{};
  for (let id in first) {
    (<any>result)[id] = (<any>first)[id];
  }
  for (let id in second) {
    if (!result.hasOwnProperty(id)) {
      (<any>result)[id] = (<any>second)[id];
    }
  }
  return result;
}

class Person {
  constructor(public name: string) {}
}
interface Loggable {
  log(): void;
}
class ConsoleLogger implements Loggable {
  log() {
    // ...
  }
}
var jim = extend(new Person("Jim"), new ConsoleLogger());
var n = jim.name;
jim.log();
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27

# 联合类型(Union Types)

联合类型表示一个值可以是几种类型之一。用竖线 | 分隔每个类型,所以 number | string | boolean 表示一个值可以是 number, string,或 boolean。

使用联合类型前:

function padLeft(value: string, padding: any) {
  if (typeof padding === "number") {
    return Array(padding + 1).join(" ") + value;
  }
  if (typeof padding === "string") {
    return padding + value;
  }
  throw new Error(`Expected string or number, got '${padding}'.`);
}

padLeft("Hello world", 4); // returns "    Hello world"
let indentedString = padLeft("Hello world", true); // 编译阶段通过,运行时报错
1
2
3
4
5
6
7
8
9
10
11
12

使用联合类型后:

function padLeft(value: string, padding: string | number) {
  // ...
}

let indentedString = padLeft("Hello world", true); // errors during compilation
1
2
3
4
5

如果一个值是联合类型,我们只能访问此联合类型的所有类型里共有的成员。

interface Bird {
  fly();
  layEggs();
}

interface Fish {
  swim();
  layEggs();
}

function getSmallPet(): Fish | Bird {
  // ...
}

let pet = getSmallPet();
pet.layEggs(); // okay
pet.swim(); // errors
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

# 类型保护与区分类型 (Type Guards and Differentiating Types)

let pet = getSmallPet();

// 每一个成员访问都会报错
if (pet.swim) {
  pet.swim();
} else if (pet.fly) {
  pet.fly();
}
1
2
3
4
5
6
7
8

# 类型断言 (Type Assertion)

为了保证上面的代码能够正常执行,可以使用断言保证类型的正确性。

  • 联合类型可以被断言为其中一个类型。
  • 父类可以被断言为子类。
  • 任何类型都可以被断言为 any。
  • any 可以被断言为任何类型。
  • 要使得 A 能够被断言为 B,只需要 A 兼容 B 或 B 兼容 A 即可。
let pet = getSmallPet();

//错误的写法
if (pet.swim) {
  //Error
  pet.swim();
  //Error
} else {
  pet.fly();
  //Error
}

//正确的写法
if ((<Fish>pet).swim) {
  //Ok
  (<Fish>pet).swim();
  //Ok
} else {
  (<Bird>pet).fly();
  //Ok
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21

双重断言

  • 任何类型都可以被断言为 any。
  • any 可以被断言为任何类型。
interface Cat {
  run(): void;
}
interface Fish {
  swim(): void;
}

function testCat(cat: Cat) {
  return cat as any as Fish;
}
1
2
3
4
5
6
7
8
9
10

举个赋值和断言区别的例子:

interface Animal {
  name: string;
}
interface Cat {
  name: string;
  run(): void;
}

const animal: Animal = {
  name: "tom",
};
let tom: Cat = animal;

// index.ts:12:5 - error TS2741: Property 'run' is missing in type 'Animal' but required in type 'Cat'.
1
2
3
4
5
6
7
8
9
10
11
12
13
14

从上例可以看出,赋值和断言的区别在于:

  • animal 断言为 Cat,只需要满足 Animal 兼容 Cat 或 Cat 兼容 Animal 即可。
  • animal 赋值给 tom,需要满足 Cat 兼容 Animal 才行。

可见类型声明是比类型断言更加严格的。所以为了增加代码的质量,我们最好优先使用类型声明,这也比类型断言的 as 语法更加优雅。

# 自定义类型保护

上面用法的弊端是我们不得不多次使用类型断言。假若我们一旦检查过类型,就能在之后的每个分支里清楚地知道 pet 的类型的话就好了。解决办法是自定义类型保护:谓词为 parameterName is Type 这种形式。比如:pet is Fish

function isFish(pet: Fish | Bird): pet is Fish {
  return (<Fish>pet).swim !== undefined;
}

// 'swim' 和 'fly' 调用都没有问题了
if (isFish(pet)) {
  pet.swim();
} else {
  pet.fly();
}
1
2
3
4
5
6
7
8
9
10

# typeof 类型保护

只有两种形式能被识别:typeof v === "typename" 和 typeof v !== "typename", "typename"必须是 "number", "string", "boolean"或 "symbol"。但是 Ts 并不会阻止你与其它字符串比较,语言不会把那些表达式识别为类型保护。

let a: Person; // Person表示类的实例类型
a.yourName;
let b: typeof Person; // typeof Person 表示类的类类型
b.myName;
// c为构造函数类型,c拥有一个构造函数,也就是new c() 返回的是Person的实例。表示c是Person类。
let c: { new (): Person } = Person;
1
2
3
4
5
6
function isNumber(x: any): x is number {
  return typeof x === "number";
}

function isString(x: any): x is string {
  return typeof x === "string";
}

function padLeft(value: string, padding: string | number) {
  if (isNumber(padding)) {
    return Array(padding + 1).join(" ") + value;
  }
  if (isString(padding)) {
    return padding + value;
  }
  throw new Error(`Expected string or number, got '${padding}'.`);
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
function padLeft(value: string, padding: string | number) {
  if (typeof padding === "number") {
    return Array(padding + 1).join(" ") + value;
  }
  if (typeof padding === "string") {
    return padding + value;
  }
  throw new Error(`Expected string or number, got '${padding}'.`);
}
1
2
3
4
5
6
7
8
9

# instanceof 类型保护

instanceof 类型保护是通过构造函数来细化类型的一种方式。instanceof 的右侧要求是一个构造函数,Ts 将细化为此构造函数的 prototype 属性的类型,如果它的类型不为 any 的话构造签名所返回的类型的联合,以此顺序。

interface Padder {
  getPaddingString(): string;
}

class SpaceRepeatingPadder implements Padder {
  constructor(private numSpaces: number) {}
  getPaddingString() {
    return Array(this.numSpaces + 1).join(" ");
  }
}

class StringPadder implements Padder {
  constructor(private value: string) {}
  getPaddingString() {
    return this.value;
  }
}

function getRandomPadder() {
  return Math.random() < 0.5
    ? new SpaceRepeatingPadder(4)
    : new StringPadder("  ");
}

// 类型为SpaceRepeatingPadder | StringPadder
let padder: Padder = getRandomPadder();

if (padder instanceof SpaceRepeatingPadder) {
  // 类型细化为'SpaceRepeatingPadder'
}
if (padder instanceof StringPadder) {
  // 类型细化为'StringPadder'
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33

# null 、undefined 、void

当你指定了--strictNullChecks 标记,null 和 undefined 只能赋值给 void 和它们各自。这能避免很多常见的问题。按照 Js 的语义,Ts 会把 null 和 undefined 区别对待。string | null,string | undefined 和 string | undefined | null 是不同的类型。

let s = "foo";
s = null; // 错误, 'null'不能赋值给'string'
let sn: string | null = "bar";
sn = null; // 可以
sn = undefined; // error, 'undefined'不能赋值给'string | null'
1
2
3
4
5

# 可选参数和可选属性

使用了--strictNullChecks 标记,可选参数会被自动地加上 undefined。

function f(x: number, y?: number) {
  return x + (y || 0);
}
f(1, 2);
f(1);
f(1, undefined);
f(1, null); // error, 'null' is not assignable to 'number | undefined'
1
2
3
4
5
6
7
class C {
  a: number;
  b?: number;
}
let c = new C();
c.a = 12;
c.a = undefined; // error, 'undefined' is not assignable to 'number'
c.b = 13;
c.b = undefined; // ok
c.b = null; // error, 'null' is not assignable to 'number | undefined'
1
2
3
4
5
6
7
8
9
10

# 任意属性

使用 [propName: string] 定义了任意属性取 string 类型的值。

需要注意的是,一旦定义了任意属性,那么确定属性和可选属性的类型都必须是它的类型的子集:

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

let tom: Person = {
  name: "Tom",
  gender: "male",
};
1
2
3
4
5
6
7
8
9
10
interface Person {
  name: string;
  age?: number;
  [propName: string]: string;
}

let tom: Person = {
  name: "Tom",
  age: 25,
  gender: "male",
};

// index.ts(3,5): 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; gender: 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'.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

上例中,任意属性的值允许是 string,但是可选属性 age 的值却是 number,number 不是 string 的子属性,所以报错了。

一个接口中只能定义一个任意属性。如果接口中有多个类型的属性,则可以在任意属性中使用联合类型:

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

let tom: Person = {
  name: "Tom",
  age: 25,
  gender: "male",
};
1
2
3
4
5
6
7
8
9
10
11

# 只读属性

有时候我们希望对象中的一些字段只能在创建的时候被赋值,那么可以用 readonly 定义只读属性:

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

let tom: Person = {
  id: 89757,
  name: "Tom",
  gender: "male",
};

tom.id = 9527;

// index.ts(14,5): error TS2540: Cannot assign to 'id' because it is a constant or a read-only property.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

上例中,使用 readonly 定义的属性 id 初始化后,又被赋值了,所以报错了。注意,只读的约束存在于第一次给对象赋值的时候,而不是第一次给只读属性赋值的时候:

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

let tom: Person = {
  name: "Tom",
  gender: "male",
};

tom.id = 89757;

// index.ts(8,5): error TS2322: Type '{ name: string; gender: string; }' is not assignable to type 'Person'.
//   Property 'id' is missing in type '{ name: string; gender: string; }'.
// index.ts(13,5): error TS2540: Cannot assign to 'id' because it is a constant or a read-only property.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

上例中,报错信息有两处,第一处是在对 tom 进行赋值的时候,没有给 id 赋值。第二处是在给 tom.id 赋值的时候,由于它是只读属性,所以报错了。

# 类型保护和类型断言去除 null

一般写法:

function f(sn: string | null): string {
  if (sn == null) {
    return "default";
  } else {
    return sn;
  }
}
1
2
3
4
5
6
7

短路写法:

function f(sn: string | null): string {
  return sn || "default";
}
1
2
3

! 空值移除:Ts 中还有一种感叹号!结尾的语法,它会从前面的表达式里移除 null 和 undefined。如果编译器不能够去除 null 或 undefined,你可以使用类型断言手动去除。语法是添加 !后缀: identifier!从 identifier 的类型里去除了 null 和 undefined。

function broken(name: string | null): string {
  function postfix(epithet: string) {
    return name.charAt(0) + '.  the ' + epithet; // error, 'name' is possibly null
  }
  name = name || "Bob";
  return postfix("great");
}

function fixed(name: string | null): string {
  function postfix(epithet: string) {
    return name!.charAt(0) + '.  the ' + epithet; // ok
  }
  name = name || "Bob";
  return postfix("great");
}

案例剖析:本例使用了嵌套函数,因为编译器无法去除嵌套函数的null(除非是立即调用的函数表达式)。
因为它无法跟踪所有对嵌套函数的调用,尤其是你将内层函数做为外层函数的返回值。
如果无法知道函数在哪里被调用,就无法知道调用时 name的类型。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19

# 类型别名

类型别名会给一个类型起个新名字。 类型别名有时和接口很像,但是可以作用于原始值,联合类型,元组以及其它任何你需要手写的类型。

type Name = string;
type NameResolver = () => string;
type NameOrResolver = Name | NameResolver;
function getName(n: NameOrResolver): Name {
  if (typeof n === "string") {
    return n;
  } else {
    return n();
  }
}
1
2
3
4
5
6
7
8
9
10

同接口一样,类型别名也可以是泛型,我们可以添加类型参数并且在别名声明的右侧传入。

type Container<T> = { value: T };
1

我们也可以使用类型别名来在属性里引用自己。

type Tree<T> = {
  value: T;
  left: Tree<T>;
  right: Tree<T>;
};
1
2
3
4
5

然而,类型别名不能出现在声明右侧的任何地方。

type Yikes = Array<Yikes>; // error
1

# 类型别名与接口

接口创建了一个新的名字,可以在其它任何地方使用。 类型别名并不创建新名字。

type Alias = { num: number };
interface Interface {
  num: number;
}
declare function aliased(arg: Alias): Alias;
declare function interfaced(arg: Interface): Interface;
1
2
3
4
5
6

类型别名不能被 extends 和 implements(自己也不能 extends 和 implements 其它类型)。

如果你无法通过接口来描述一个类型并且需要使用联合类型或元组类型,这时通常会使用类型别名。

# 字符串字面量类型

字符串字面量类型允许你指定字符串必须的固定值。 在实际应用中,字符串字面量类型可以与联合类型,类型保护和类型别名很好的配合。 通过结合使用这些特性,你可以实现类似枚举类型的字符串。

type Easing = "ease-in" | "ease-out" | "ease-in-out";
class UIElement {
  animate(dx: number, dy: number, easing: Easing) {
    if (easing === "ease-in") {
      // ...
    } else if (easing === "ease-out") {
    } else if (easing === "ease-in-out") {
    } else {
      // error! should not pass null or undefined.
    }
  }
}

let button = new UIElement();
button.animate(0, 0, "ease-in");
button.animate(0, 0, "uneasy"); // error: "uneasy" is not allowed here
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

字符串字面量类型还可以用于区分函数重载

function createElement(tagName: "img"): HTMLImageElement;
function createElement(tagName: "input"): HTMLInputElement;
// ... more overloads ...
function createElement(tagName: string): Element {
  // ... code goes here ...
}
1
2
3
4
5
6

# 数字字面量类型

function rollDie(): 1 | 2 | 3 | 4 | 5 | 6 {
  // ...
}
1
2
3

# 可辨识联合(Discriminated Unions)

你可以合并单例类型,联合类型,类型保护和类型别名来创建一个叫做可辨识联合的高级模式,它也称做标签联合或代数数据类型。

interface Square {
  kind: "square";
  size: number;
}
interface Rectangle {
  kind: "rectangle";
  width: number;
  height: number;
}
interface Circle {
  kind: "circle";
  radius: number;
}

type Shape = Square | Rectangle | Circle;

可辨识联合;

function area(s: Shape) {
  switch (s.kind) {
    case "square":
      return s.size * s.size;
    case "rectangle":
      return s.height * s.width;
    case "circle":
      return Math.PI * s.radius ** 2;
  }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28

覆盖完整分支

type Shape = Square | Rectangle | Circle | Triangle;

function assertNever(x: never): never {
  throw new Error("Unexpected object: " + x);
}
function area(s: Shape) {
  switch (s.kind) {
    case "square":
      return s.size * s.size;
    case "rectangle":
      return s.height * s.width;
    case "circle":
      return Math.PI * s.radius ** 2;
    default:
      return assertNever(s); // error here if there are missing cases
  }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

# 多态的 this 类型

多态的 this 类型表示的是某个包含类或接口的子类型。这被称做 F-bounded 多态性。 它能很容易的表现连贯接口间的继承。在计算器的例子里,在每个操作之后都返回 this 类型。

class BasicCalculator {
  public constructor(protected value: number = 0) {}
  public currentValue(): number {
    return this.value;
  }
  public add(operand: number): this {
    this.value += operand;
    return this;
  }
  public multiply(operand: number): this {
    this.value *= operand;
    return this;
  }
  // ... other operations go here ...
}

let v = new BasicCalculator(2).multiply(5).add(1).currentValue();
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

由于这个类使用了 this 类型,你可以继承它,新的类可以直接使用之前的方法,不需要做任何的改变。

class ScientificCalculator extends BasicCalculator {
  public constructor(value = 0) {
    super(value);
  }
  public sin() {
    this.value = Math.sin(this.value);
    return this;
  }
  // ... other operations go here ...
}

let v = new ScientificCalculator(2).multiply(5).sin().add(1).currentValue();
1
2
3
4
5
6
7
8
9
10
11
12

如果没有 this 类型, ScientificCalculator 就不能够在继承 BasicCalculator 的同时还保持接口的连贯性。 multiply 将会返回 BasicCalculator,它并没有 sin 方法。 然而,使用 this 类型, multiply 会返回 this,在这里就是 ScientificCalculator。

在 Ts 函数的参数上同样会存在一个 this 的关键字,假使我们需要为一个函数定义它的 this 类型。我们可以这样做:在函数的参数上,使用 this 关键字命名,注意这里必须被命名为 this 并且 this 必须放在函数第一个参数位上。

// 我希望函数中的 this 指向 { name:'19Qingfeng' }
type ThisPointer = { name: "19Qingfeng" };
function counter(this: ThisPointer, age: number) {
  console.log(this.name); // 此时TS会推断出this的类型为 ThisPointer
}
1
2
3
4
5

# 映射类型

一个常见的任务是将一个已知的类型每个属性都变为可选的或者是只读的版本。从旧类型中创建新类型的一种方式:映射类型。

type Readonly<T> = {
  readonly [P in keyof T]: T[P];
};
type Partial<T> = {
  [P in keyof T]?: T[P];
};
type PersonPartial = Partial<Person>; // 类型映射
type ReadonlyPerson = Readonly<Person>; // 类型映射
1
2
3
4
5
6
7
8

在真正的应用里,可能不同于上面的 Readonly 或 Partial。 它们会基于一些已存在的类型,且按照一定的方式转换字段。 这就是 keyof 和索引访问类型要做的事情。

type NullablePerson = { [P in keyof Person]: Person[P] | null };
type PartialPerson = { [P in keyof Person]?: Person[P] };
1
2

上面例子更通用的写法:

type Nullable<T> = { [P in keyof T]: T[P] | null };
type Partial<T> = { [P in keyof T]?: T[P] };

在上面这些例子里,属性列表是 keyof T 且结果类型是 T[P]的变体。 这是使用通用映射类型的一个好模版。 因为这类转换是同态的,映射只作用于 T 的属性而没有其它的。编译器知道在添加任何新属性之前可以拷贝所有存在的属性修饰符。 例如,假设 Person.name 是只读的,那么 Partial<Person>.name 也将是只读的且为可选的。
1
2
3
4

keyof:Ts 中 keyof 操作符会将一个对象类型(注意这里是类型并不是值)的 key 组成联合类型返回。

interface IProps {
  name: string;
  count: number;
}
type Ikea = keyof IProps; // Ikea = 'name' | 'count'
function testKeyof(props: Ikea): void {}
1
2
3
4
5
6

Partial:源码接受传入的一个泛型类型 T ,使用 in 关键字遍历传入的 T 类型,重新定义了一个相同的类型,不过新的类型所有的 key 变成了可选类型。

type Partial<T> = {
  [P in keyof T]?: T[P];
};
1
2
3
type Coord = Partial<Record<"x" | "y", number>>;
// 等同于
type Coord = {
  x?: number;
  y?: number;
};
1
2
3
4
5
6
type Pick<T, K extends keyof T> = {
  [P in K]: T[P];
};
type Record<K extends string, T> = {
  [P in K]: T;
};
1
2
3
4
5
6

Readonly, Partial 和 Pick 是同态的,但 Record 不是。 因为 Record 并不需要输入类型来拷贝属性,所以它不属于同态。非同态类型本质上会创建新的属性,因此它们不会从它处拷贝属性修饰符。

type ThreeStringProps = Record<"prop1" | "prop2" | "prop3", string>;
1

# 索引类型

使用索引类型,编译器就能够检查使用了动态属性名的代码。

function pluck(o, names) {
  return names.map((n) => o[n]);
}
1
2
3
function pluck<T, K extends keyof T>(o: T, names: K[]): T[K][] {
  return names.map((n) => o[n]);
}

interface Person {
  name: string;
  age: number;
}
let person: Person = {
  name: "Jarid",
  age: 35,
};
let strings: string[] = pluck(person, ["name"]); // ok, string[]
1
2
3
4
5
6
7
8
9
10
11
12
13

# 装饰器

随着 TS 和 ES6 里引入了类,在一些场景下我们需要额外的特性来支持标注或修改类及其成员。

装饰器为我们在类的声明及成员上通过元编程语法添加标注提供了一种方式。装饰器使用 @expression 这种形式,expression 求值后必须为一个函数,它会在运行时被调用,被装饰的声明信息做为参数传入。

装饰器本质上是一种特殊的函数被应用在类、属性、方法、访问器、方法参数上。所以应用装饰器其实很像是组合一系列函数,类似于高阶函数和类。 通过装饰器我们可以轻松实现代理模式来使代码更简洁以及实现其它一些更有趣的能力。

一共有 5 种装饰器可被我们使用: 类装饰器、属性装饰器、方法装饰器、访问器装饰器、参数装饰器。

单独的参数装饰器能做的事情很有限,它一般都被用于记录可被其它装饰器使用的信息。

装饰器只在解释执行时应用一次。

// 类装饰器
@classDecorator
class Bird {
  // 属性装饰器
  @propertyDecorator
  name: string;

  // 方法装饰器
  @methodDecorator
  fly(
    // 参数装饰器
    @parameterDecorator
    meters: number
  ) {}

  // 访问器装饰器
  @accessorDecorator
  get egg() {}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
点开查看:这里的代码会在终端中打印 apply decorator,即便我们其实并没有使用类 A。
function f(C) {
  console.log("apply decorator");
  return C;
}

@f
class A {}

// output: apply decorator
1
2
3
4
5
6
7
8
9

# 类装饰器

类型声明:类装饰器适用于继承一个现有类并添加一些属性和方法。

type ClassDecorator = <TFunction extends Function>(target: TFunction) => TFunction | void;

@参数:
    target: 类的构造器。
@返回:
    如果类装饰器返回了一个值,它将会被用来代替原有的类构造器的声明。
1
2
3
4
5
6

普通饰器,不能传参数

// 定义一个装饰器
function logClass(param: any) {
  console.log(param); //输出:ƒ HttpClient() {}
  // 扩展类的属性与方法
  param.prototype.apiUrl = "xxx";
  param.prototype.run = function () {
    console.log("动态扩展的run方法");
  };
}

@logClass
class HttpClient {
  constructor() {}
  getData() {}
}

let h: any = new HttpClient();
h.run(); //动态扩展的run方法
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

装饰器工厂,可以传参

// 定义一个装饰器工厂
// param接收自己传的参数
// target接收类信息
function logClass(param: string) {
  return function (target: any) {
    console.log(param, target); //xxx ƒ HttpClient() {}
    // 属性扩展
    target.prototype.url = param;
  };
}

@logClass("xxx")
class HttpClient {
  constructor() {}
  getData() {}
}

let h: any = new HttpClient();
console.log(h.url); //xxx
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19

使用装饰器重载类方法

// 定义一个装饰器类重载方法
function logClass(param: any) {
  return class extends param {
    api: any = "修饰器修改的api";
    getData() {
      console.log("装饰器重载的方法");
    }
  };
}

@logClass
class HttpClient {
  api: string;
  constructor() {
    this.api = "构造函数的api";
  }
  getData() {}
}

let h: any = new HttpClient();
h.getData(); //装饰器重载的方法
console.log(h.api); //修饰器修改的api
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22

覆盖常用的原有 toString 方法

// 例如我们可以添加一个 toString 方法给所有的类来覆盖它原有的 toString 方法。
type Consturctor = { new (...args: any[]): any };

function toString<T extends Consturctor>(BaseClass: T) {
  return class extends BaseClass {
    toString() {
      return JSON.stringify(this);
    }
  };
}

@toString
class C {
  public foo = "foo";
  public num = 24;
}

console.log(new C().toString());
// -> {"foo":"foo","num":24}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19

没有类型保护

// 遗憾的是装饰器并没有类型保护,这意味着
declare function Blah<T>(target: T): T & { foo: number };

@Blah
class Foo {
  bar() {
    return this.foo; // Property 'foo' does not exist on type 'Foo'
  }
}

new Foo().foo; // Property 'foo' does not exist on type 'Foo'
1
2
3
4
5
6
7
8
9
10
11

# 属性装饰器

类型定义

type PropertyDecorator = (target: Object, propertyKey: string | symbol) => void;

@参数:
    target: 对于静态成员来说是类的构造器,对于实例成员来说是类的原型链。
    propertyKey: 属性的名称。
@返回:
    返回的结果将被忽略
1
2
3
4
5
6
7

接收两个参数

// 属性装饰器
// target接收类的原型对象
// param接收自己传入参数值
// attr接收修饰的属性名称
function logProps(param: any) {
  return (target: any, attr: any) => {
    console.log(param, target, attr); //xxx {getData: ƒ, constructor: ƒ} api
    // 修改属性
    target[attr] = param;
  };
}
// @logClass
class HttpClient {
  @logProps("xxx")
  api: string | undefined;
  constructor() {}
  getData() {
    console.log(this.api);
  }
}

let h: any = new HttpClient();
h.getData(); //xxx
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
function capitalizeFirstLetter(str: string) {
  return str.charAt(0).toUpperCase() + str.slice(1);
}

function observable(target: any, key: string): any {
  // prop -> onPropChange
  const targetKey = "on" + capitalizeFirstLetter(key) + "Change";

  target[targetKey] = function (fn: (prev: any, next: any) => void) {
    let prev = this[key];
    Reflect.defineProperty(this, key, {
      set(next) {
        fn(prev, next);
        prev = next;
      },
    });
  };
}

class C {
  @observable
  foo = -1;

  @observable
  bar = "bar";
}

const c = new C();

c.onFooChange((prev, next) => console.log(`prev: ${prev}, next: ${next}`));
c.onBarChange((prev, next) => console.log(`prev: ${prev}, next: ${next}`));

c.foo = 100; // -> prev: -1, next: 100
c.foo = -3.14; // -> prev: 100, next: -3.14
c.bar = "baz"; // -> prev: bar, next: baz
c.bar = "sing"; // -> prev: baz, next: sing
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36

# 方法装饰器

类型定义

type MethodDecorator = <T>(
  target: Object,
  propertyKey: string | symbol,
  descriptor: TypedPropertyDescriptor<T>
) => TypedPropertyDescriptor<T> | void;

@参数:
    target: 对于静态成员来说是类的构造器,对于实例成员来说是类的原型链。
    propertyKey: 属性的名称。
    descriptor: 属性的描述器。
@返回: 如果返回了值,它会被用于替代属性的描述器。
1
2
3
4
5
6
7
8
9
10
11
// 方法装饰器
// param:传入参数
// target:被修饰的是静态成员,则是类的构造函数
// 被修饰的是实例成员,则是类的原型对象
// methodName:被修饰方法的方法名
// des:成员的属性描述符(des.value是当前方法)
function logMethod(param: any) {
  return function (target: any, methodName: string, des: any) {
    console.log(param, target, methodName, des);
    // 扩展属性
    target.url = "xxsss";
    // 扩展方法
    target.run = function () {
      console.log("这是扩展的方法");
    };
    // 修改装饰器的方法
    // 1.保存当前方法
    let md = des.value;
    //将原方法的参数替换为string类型
    des.value = function (...args: any[]) {
      args = args.map((item) => String(item));
      console.log(args);
      // 对象冒充,来修改方法,若不写,则将会替换原方法
      md.apply(this, args);
    };
  };
}
class HttpClient {
  api: string | undefined;
  constructor() {}
  @logMethod("xxxx")
  getData() {
    console.log(this.api);
  }
}

let h: any = new HttpClient();
h.run(); //这是扩展的方法
console.log(h.url); //xxsss
h.getData(123, 665);
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
function logger(
  target: any,
  propertyKey: string,
  descriptor: PropertyDescriptor
) {
  const original = descriptor.value;

  descriptor.value = function (...args) {
    console.log("params: ", ...args);
    const result = original.call(this, ...args);
    console.log("result: ", result);
    return result;
  };
}

class C {
  @logger
  add(x: number, y: number) {
    return x + y;
  }
}

const c = new C();
c.add(1, 2);
// -> params: 1, 2
// -> result: 3
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26

# 访问器装饰器

访问器装饰器总体上讲和方法装饰器很接近,唯一的区别在于描述器中有的 key 不同

方法装饰器的描述器的 key 为:value\writable\enumerable\configurable

访问器装饰器的描述器的 key 为:get\set\enumerable\configurable

function immutable(
  target: any,
  propertyKey: string,
  descriptor: PropertyDescriptor
) {
  const original = descriptor.set;

  descriptor.set = function (value: any) {
    return original.call(this, { ...value });
  };
}

class C {
  private _point = { x: 0, y: 0 };

  @immutable
  set point(value: { x: number; y: number }) {
    this._point = value;
  }

  get point() {
    return this._point;
  }
}

const c = new C();
const point = { x: 1, y: 1 };
c.point = point;

console.log(c.point === point);
// -> false
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31

# 参数装饰器

类型声明

type ParameterDecorator = (
  target: Object,
  propertyKey: string | symbol,
  parameterIndex: number
) => void;

@参数:
    target: 对于静态成员来说是类的构造器,对于实例成员来说是类的原型链。
    propertyKey: 属性的名称(注意是方法的名称,而不是参数的名称)。
    parameterIndex: 参数在方法中所处的位置的下标。
@返回:
    返回的值将会被忽略
1
2
3
4
5
6
7
8
9
10
11
12
// 方法参数装饰器
// 在执行方法时,调用方法参数装饰器,给类的原型对象增加属性,也可以修改参数
// param:传入参数
// target:被修饰的是静态成员,则是类的构造函数
// 被修饰的是实例成员,则是类的原型对象
// methodName:被修饰方法的方法名
// paramIndex:参数索引下标
function logParams(param: any) {
  return function (target: any, methodName: any, paramIndex: any) {
    console.log(param, target, methodName, paramIndex);
    // 给原型对象增加属性
    target.id = param;
  };
}

class HttpClient {
  url: string | undefined;
  get(@logParams("uuid") uuid: any) {
    console.log("类里面的实例方法" + uuid);
  }
}
let h: any = new HttpClient();
h.get(12233); //类里面的实例方法12233
console.log(h.id); //uuid
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24

# 执行顺序

装饰器可以对类、方法、属性、参数进行修改、扩展、替换等操作

执行顺序:属性装饰器->方法装饰器->方法参数装饰器->类装饰器

同类装饰器执行顺序:由下向上、由里及外、由后往前

111

不同类型的装饰器的执行顺序是明确定义的:

  1. 实例成员:参数装饰器 --> (方法 / 访问器 / 属性) 装饰器。
  2. 静态成员: 参数装饰器 --> (方法 / 访问器 / 属性) 装饰器。
  3. 构造器: 参数装饰器。
  4. 类装饰器。
function f(key: string): any {
  console.log("evaluate: ", key);
  return function () {
    console.log("call: ", key);
  };
}

@f("Class Decorator")
class C {
  @f("Static Property")
  static prop?: number;

  @f("Static Method")
  static method(@f("Static Method Parameter") foo) {}

  constructor(@f("Constructor Parameter") foo) {}

  @f("Instance Method")
  method(@f("Instance Method Parameter") foo) {}

  @f("Instance Property")
  prop?: number;
}

输出信息为:
evaluate:  Instance Method
evaluate:  Instance Method Parameter
call:  Instance Method Parameter
call:  Instance Method
evaluate:  Instance Property
call:  Instance Property
evaluate:  Static Property
call:  Static Property
evaluate:  Static Method
evaluate:  Static Method Parameter
call:  Static Method Parameter
call:  Static Method
evaluate:  Class Decorator
evaluate:  Constructor Parameter
call:  Constructor Parameter
call:  Class Decorator
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
function f(key: string): any {
  console.log("evaluate: ", key);
  return function () {
    console.log("call: ", key);
  };
}

class C {
  method(
    @f("Parameter Foo") foo,
    @f("Parameter Bar") bar
  ) {}
}

输出信息为:
evaluate:  Parameter Foo
evaluate:  Parameter Bar
call:  Parameter Bar
call:  Parameter Foo
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19

# 多个装饰器的组合

你可以对同一目标应用多个装饰器,它们的组合顺序为:求值外层装饰器、求值内层装饰器、调用内层装饰器、调用外层装饰器。

function f(key: string) {
  console.log("evaluate: ", key);
  return function () {
    console.log("call: ", key);
  };
}

class C {
  @f("Outer Method")
  @f("Inner Method")
  method() {}
}

输出信息为:
evaluate: Outer Method
evaluate: Inner Method
call: Inner Method
call: Outer Method
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

# 元数据

通常来说元数据和装饰器是 EcmaScript 中两个独立的部分, 然而如果你想实现像是反射这样的能力,你总是同时需要它们。有了 reflect-metadata 的帮助, 我们可以获取编译期的类型。

import "reflect-metadata";

function validate(target: Object, key: string, descriptor: PropertyDescriptor) {
  const originalFn = descriptor.value;

  // 获取参数的编译期类型
  const designParamTypes = Reflect.getMetadata(
    "design:paramtypes",
    target,
    key
  );

  descriptor.value = function (...args: any[]) {
    args.forEach((arg, index) => {
      const paramType = designParamTypes[index];

      const result = arg.constructor === paramType || arg instanceof paramType;

      if (!result) {
        throw new Error(
          `Failed for validating parameter: ${arg} of the index: ${index}`
        );
      }
    });

    return originalFn.call(this, ...args);
  };
}

class C {
  @validate
  sayRepeat(word: string, x: number) {
    return Array(x).fill(word).join("");
  }
}

const c = new C();
c.sayRepeat("hello", 2); // pass
c.sayRepeat("", "lol" as any); // throw an error
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39

# 声明文件

  • declare var 声明全局变量
  • declare function 声明全局方法
  • declare class 声明全局类
  • declare enum 声明全局枚举类型
  • declare namespace 声明(含有子属性的)全局对象
  • interface 和 type 声明全局类型
  • export 导出变量
  • export namespace 导出(含有子属性的)对象
  • export default ES6 默认导出
  • export = commonjs 导出模块
  • export as namespace UMD 库声明全局变量
  • declare global 扩展全局变量
  • declare module 扩展模块
  • /// 三斜线指令

声明文件必需以 .d.ts 为后缀:

// src/jQuery.d.ts
declare var jQuery: (selector: string) => any;
1
2

# 其他特征

# 迭代器相关

for..of 和 for..in 均可迭代一个列表,
但是用于迭代的值却不同,
for..in 迭代的是对象的键的列表,
for..of 则迭代对象的键对应的值。
1
2
3
4

# 函数相关

函数表达式:注意不要混淆了 TypeScript 中的 => 和 ES6 中的 =>。在 TypeScript 的类型定义中,=> 用来表示函数的定义,左边是输入类型,需要用括号括起来,右边是输出类型。在 ES6 中,=> 叫做箭头函数,应用十分广泛,可以参考 ES6 中的箭头函数。

let mySum: (x: number, y: number) => number = function (
  x: number,
  y: number
): number {
  return x + y;
};
1
2
3
4
5
6

用接口定义函数的形状:采用函数表达式|接口定义函数的方式时,对等号左侧进行类型限制,可以保证以后对函数名赋值时保证参数个数、参数类型、返回值类型不变。

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

let mySearch: SearchFunc;
mySearch = function (source: string, subString: string) {
  return source.search(subString) !== -1;
};
1
2
3
4
5
6
7
8

函数重载:重复定义了多次函数 reverse,前几次都是函数定义,最后一次是函数实现。在编辑器的代码提示中,可以正确的看到前两个提示。

function reverse(x: number): number;
function reverse(x: string): string;
function reverse(x: number | string): number | string | void {
  if (typeof x === "number") {
    return Number(x.toString().split("").reverse().join(""));
  } else if (typeof x === "string") {
    return x.split("").reverse().join("");
  }
}
1
2
3
4
5
6
7
8
9

注意,TypeScript 会优先从最前面的函数定义开始匹配,所以多个函数定义如果有包含关系,需要优先把精确的定义写在前面。

# 接口相关

使用含有泛型的接口来定义函数的形状:

interface CreateArrayFunc {
  <T>(length: number, value: T): Array<T>;
}

let createArray: CreateArrayFunc;
createArray = function <T>(length: number, value: T): Array<T> {
  let result: T[] = [];
  for (let i = 0; i < length; i++) {
    result[i] = value;
  }
  return result;
};

createArray(3, "x"); // ['x', 'x', 'x']
1
2
3
4
5
6
7
8
9
10
11
12
13
14

进一步,我们可以把泛型参数提前到接口名上:

interface CreateArrayFunc<T> {
  (length: number, value: T): Array<T>;
}

let createArray: CreateArrayFunc<any>;
createArray = function <T>(length: number, value: T): Array<T> {
  let result: T[] = [];
  for (let i = 0; i < length; i++) {
    result[i] = value;
  }
  return result;
};

createArray(3, "x"); // ['x', 'x', 'x']
1
2
3
4
5
6
7
8
9
10
11
12
13
14

注意,此时在使用泛型接口的时候,需要定义泛型的类型。

用接口表示数组

interface NumberArray {
  [index: number]: number;
}
let fibonacci: NumberArray = [1, 1, 2, 3, 5];
1
2
3
4

# 模块相关

大家最熟知的JavaScript模块加载器是服务于Node.js的 CommonJS和服务于Web应用的Require.js。

# new 关键字

new 关键字用在类型上,表示构造函数的类型。ts 中 new() 表示构造函数类型。当我们声明一个类的时候,其实声明的是这个类的实例类型和静态类型两个类型。

class Star {
  constructor() {
    // init somethings
  }
}
// 此时这里的example参数它的类型为 Star 类类型而非实例类型
// 它表示接受一个构造函数 这个构造函数new后会返回Star的实例类型
function counter(example: new () => Star) {
  // do something
}
// 直接传入类
counter(Star);
1
2
3
4
5
6
7
8
9
10
11
12

# infer

infer 表示在 extends 条件语句中待推断的类型变量,必须联合 extends 类型出现。

其实碰到 infer 关键字简单的将它理解为一个等待推断的类型(我现在不知道,得根据条件( extends )来判断)就可以了。

重点是: infer 跟随 extends 成双出现。infer P 表示类型 P 是一个待推断的类型。(不使用 infer 直接 P 会报错)

type ParamType<T> = T extends (...args: infer P) => any ? P : T;

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

type Func = (user: User) => void;

type Param = ParamType<Func>; // Param = User
type AA = ParamType<string>; // string
1
2
3
4
5
6
7
8
9
10
11

# extends

Ts 中 extends 除了用在继承上,还可以表达泛型约束,通过 extends 关键字可以约束泛型具有某些属性。 其实 extends 关键字表示约束的时候,就是表示要求泛型上必须实现(包含)约束的属性。

function loggingIdentity<T>(arg: T): T {
  console.log(arg.length) // Ts语法错误 T可以是任意类型,并不存在length属性
  return arg
}

interface Lengthwise {
  length: number
}
// 表示传入的泛型T接受Lengthwise的约束
// T必须实现Lengthwise 换句话说 Lengthwise这个类型是完全可以赋给T
function loggingIdentity<T extends Lengthwise>(arg: T): T {
  console.log(arg.length) // OK
  return arg
}

loggingIdentity(3);  // Error
loggingIdentity({length: 10, value: 3}) // OK

再比如下面的例子,你可以声明一个类型参数,且它被另一个类型参数所约束

function getProperty<T, K extends keyof T> (obj: T, key: K ) {
  return obj[key]
}

let x = {a: 1, b: 2, c: 3, d: 4}

getProperty(x, 'a') // okay
getProperty(x, 'm') // error

同样 extends 还可以用在类型判断语句上:

type Env<T> = T extends 'production' ? 'production' : 'development';
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32

# 在构造函数的参数上使用 public 等同于创建了同名的成员变量

class Student {
  fullName: string;
  constructor(
    public firstName: string,
    public middleInitial: string,
    public lastName: string
  ) {
    this.fullName = firstName + " " + middleInitial + " " + lastName;
  }
}
1
2
3
4
5
6
7
8
9
10

# any 、{} 、 Object

any比较特殊,它是最普通的类型但是允许你在上面做任何事情。也就是说你可以在上面调用,构造它,访问它的属性等等。 记住,当你使用any时,你会失去大多数TypeScript提供的错误检查和编译器支持。

如果你还是决定使用Object和{},你应该选择{}。虽说它们基本一样,但是从技术角度上来讲{}在一些深奥的情况里比Object更普通。
1
2
3

# 变量的暂时性死区

拥有块级作用域的变量的另一个特点是,它们不能在被声明之前读或写。虽然这些变量始终“存在”于它们的作用域里,但在直到声明它的代码之前的区域都属于暂时性死区。
1

# 解构数组

let input = [1, 2];
let [first, second] = input;
console.log(first); // outputs 1
console.log(second); // outputs 2
1
2
3
4

# ReadonlyArray<T>

let a: number[] = [1, 2, 3, 4];
let ro: ReadonlyArray<number> = a;
ro[0] = 12; // error!
ro.push(5); // error!
ro.length = 100; // error!
a = ro; // error!
1
2
3
4
5
6

# readonly vs const

最简单判断该用readonly还是const的方法是看要把它做为变量使用还是做为一个属性。 做为变量使用的话用const,若做为属性则使用readonly
1
上次更新: 2025/02/10, 20:20:37
最近更新
01
Git问题集合
01-29
02
安装 Nginx 服务器
01-25
03
安装 Docker 容器
01-25
更多文章>
×
×