TypeScript

前言

    TypeScript 基础知识学习,来源为官方文档,用于定期复习巩固。


主要内容

基本类型

类型

  • 布尔值
  • 数字
  • 字符串
  • 数组:number[], Array
  • 元祖:[string, number],联合类型
  • 枚举:enum Color{Red, Green, Black}
  • any
  • void: 只能赋值为 null 或者 undefined
  • null 和 undefined: 如果不打开 –strictNullChecks,null 和 undefined 为所有类型的子类型
  • never
  • object

类型断言

两种写法,someValue 本身可能不是 string 类型,用户直接断言其为 string 类型:

  • let strLength: number = (<string>someValue).length
  • let strLength: number = (someValue as string).length

变量声明

变量声明

关键词:var, let, const

解构赋值

  • 解构数组
  • 解构对象:默认值、重命名、类型

接口

初探

  • 检查必须存在的属性是否存在
  • 不检查属性顺序

可选属性

  • 可选属性

只读属性

  • 只读属性:只读变量用 const,只读属性用 readonly
1
2
3
4
interface Point {
readonly x: number;
y?: string;
}

额外的属性检查

  • 对象字面量作为参数传递时会经过额外的属性检查,如果对象字面量中包含目标类型中没有的类型则会报错
  • 解决办法:类型断言、字符串索引签名、赋值给一个变量

函数类型

  • 形式
1
2
3
interface SearchFunc {
(source: string, subString: string): boolean;
}
  • 参数名不需要匹配,但对应位置的属性需要是兼容的

可索引的类型

  • 索引签名,比如下面的 index
1
2
3
interface StringArray {
[index: number]: string;
}
  • 索引签名有两种:数字和字符串
  • 可以同时使用两种类型的索引,但是数字索引的返回值必须是字符串索引返回值类型的子类型,因为 js 会把数字索引转化成字符串索引

类类型

接口描述了类的公共部分,而不是公共和私有两部分。 它不会帮你检查类是否具有某些私有成员

类静态部分与实例部分的区别(?云里雾里)

继承接口

  • 可继承一个或多个接口

混合类型

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
interface Counter {
(start: number): string;
interval: number;
reset(): void;
}

function getCounter(): Counter {
let counter = <Counter>function(start: number) {};
counter.interval = 123;
counter.reset = function() {};
return counter;
}

let c = getCounter();
c(10);
c.reset();
c.interval = 5.0;

接口继承类(?)

1
2
3
4
5
6
7
8
9
10
11
class Greeter {
greeting: string;
constructor(message: string) {
this.greeting = message;
}
greet() {
return "Hello, " + this.greeting;
}
}

let greeter = new Greeter("world");

继承

  • 派生类(子类),基类(超类)
  • 派生类包含了一个构造函数,它必须调用 super(),它会执行基类的构造函数

公共,私有与受保护的修饰符

  • 默认为 public: 在 TypeScript 里,成员都默认为 public
  • 理解 private:当成员被标记成 private 时,它就不能在声明它的类的外部访问
  • 当我们比较两种不同的类型时,并不在乎它们从何处而来,如果所有成员的类型都是兼容的,我们就认为它们的类型是兼容的
  • 如果其中一个类型里包含一个 private 成员,那么只有当另外一个类型中也存在这样一个 private 成员, 并且它们都是来自同一处声明时,我们才认为这两个类型是兼容的。 对于 protected 成员也使用这个规则
  • protected 成员在派生类中仍然可以访问
  • 构造函数也可以被标记成 protected。 这意味着这个类不能在包含它的类外被实例化,但是能被继承

readonly 修饰符

  • 使用 readonly 关键字将属性设置为只读的。 只读属性必须在声明时或构造函数里被初始化
  • 参数属性:参数属性通过给构造函数参数前面添加一个访问限定符来声明(public, private, protected)
1
2
3
4
5
6
7
8
9
10
11
12
13
class Octopus {
readonly numberOfLegs: number = 8;
constructor(readonly name: string) {}
}

// 不用参数属性表述如下,和上面的类是一样的
class Octopus {
readonly name: string;
readonly numberOfLegs: number = 8;
constructor(theName: string) {
this.name = theName;
}
}

存取器

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
let passcode = "secret passcode";

class Employee {
private _fullName: string;

get fullName(): string {
return this._fullName;
}

set fullName(newName: string) {
if (passcode && passcode == "secret passcode") {
this._fullName = newName;
} else {
console.log("Error: Unauthorized update of employee!");
}
}
}

let employee = new Employee();
employee.fullName = "Bob Smith";
if (employee.fullName) {
alert(employee.fullName);
}
  • TypeScript 支持通过 getters/setters 来截取对对象成员的访问。 它能帮助你有效的控制对对象成员的访问
  • 存取器要求你将编译器设置为输出 ECMAScript 5 或更高。 不支持降级到 ECMAScript 3
  • 只带有 get 不带有 set 的存取器自动被推断为 readonly

静态属性

  • 存在于类本身上面而不是类的实例上
  • 实例想要访问这个属性的时候,都要在前面加上类名

抽象类

  • 抽象类做为其它派生类的基类使用。 它们一般不会直接被实例化。 不同于接口,抽象类可以包含成员的实现细节
  • 抽象类中的抽象方法不包含具体实现并且必须在派生类中实现

高级技巧

  • 构造函数
  • 把类当接口使用

函数

为函数定义类型

1
2
3
4
5
6
7
function add(x: number, y: number): number {
return x + y;
}

let myAdd = function(x: number, y: number): number {
return x + y;
};
  • 我们可以给每个参数添加类型之后再为函数本身添加返回值类型。 TypeScript 能够根据返回语句自动推断出返回值类型,因此我们通常省略它
    完整的写法如下:
1
2
3
4
5
6
let myAdd: (x: number, y: number) => number = function(
x: number,
y: number
): number {
return x + y;
};
  • 只要参数类型是匹配的,那么就认为它是有效的函数类型,而不在乎参数名是否正确

推断类型

  • 在赋值语句的一边指定了类型但是另一边没有类型的话,TypeScript 编译器会自动识别出类型。这叫做【按上下文归类】

可选参数和默认参数

  • 可选参数必须跟在必须参数后面
  • 带默认值的参数不需要放在必须参数的后面

剩余参数

1
2
3
4
5
function buildName(firstName: string, ...restOfName: string[]) {
return firstName + " " + restOfName.join(" ");
}

let employeeName = buildName("Joseph", "Samuel", "Lucas", "MacKinzie");

this(?需要多复习)

  • JavaScript 里,this 的值在函数被调用的时候才会指定
  • 箭头函数能保存函数创建时的 this 值,而不是调用时的值
  • this 参数在回调函数中

重载

  • 为同一个函数提供多个函数类型定义来进行函数重载
  • 它查找重载列表,尝试使用第一个重载定义。 如果匹配的话就使用这个。因此,在定义重载的时候,一定要把最精确的定义放在最前面
  • 注意,function pickCard(x): any 并不是重载列表的一部分,因此这里只有两个重载:一个是接收对象另一个接收数字。 以其它参数调用 pickCard 会产生错误。
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
let suits = ["hearts", "spades", "clubs", "diamonds"];

function pickCard(x: { suit: string; card: number }[]): number;
function pickCard(x: number): { suit: string; card: number };
function pickCard(x): any {
// Check to see if we're working with an object/array
// if so, they gave us the deck and we'll pick the card
if (typeof x == "object") {
let pickedCard = Math.floor(Math.random() * x.length);
return pickedCard;
}
// Otherwise just let them pick the card
else if (typeof x == "number") {
let pickedSuit = Math.floor(x / 13);
return { suit: suits[pickedSuit], card: x % 13 };
}
}

let myDeck = [
{ suit: "diamonds", card: 2 },
{ suit: "spades", card: 10 },
{ suit: "hearts", card: 4 }
];
let pickedCard1 = myDeck[pickCard(myDeck)];
alert("card: " + pickedCard1.card + " of " + pickedCard1.suit);

let pickedCard2 = pickCard(15);
alert("card: " + pickedCard2.card + " of " + pickedCard2.suit);

泛型

泛型之 HelloWorld

  • 类型变量:只用于表示类型而不是值
1
2
3
function identity<T>(arg: T): T {
return arg;
}
  • 泛型函数
1
2
3
function identity<T>(arg: T): T {
return arg;
}
  • 使用方法:
    • 传入所有参数:let output = identity<string>("myString")
    • 使用类型推论:let output = identity("myString");

使用泛型变量

  • 我们把泛型变量 T 当做类型的一部分使用,而不是整个类型,增加了灵活性。下面的例子中 T 表示的就是数组中元素的类型而非入参的类型。
1
2
3
4
function loggingIdentity<T>(arg: T[]): T[] {
console.log(arg.length); // Array has a .length, so no more error
return arg;
}

泛型类型

  • 可以使用不同的泛型参数名,只要在数量上和使用方式上能对应上就可以。通用用 T 表示
1
2
3
4
5
function identity<T>(arg: T): T {
return arg;
}

let myIdentity: <U>(arg: U) => U = identity;
  • 可以使用带有调用签名的对象字面量来定义泛型函数:
1
2
3
4
5
function identity<T>(arg: T): T {
return arg;
}

let myIdentity: { <T>(arg: T): T } = identity;
  • 可以将泛型参数当作整个接口的一个参数,接口里的其它成员也能知道这个参数的类型
1
2
3
4
5
6
7
8
9
interface GenericIdentityFn<T> {
(arg: T): T;
}

function identity<T>(arg: T): T {
return arg;
}

let myIdentity: GenericIdentityFn<number> = identity;
  • 除了泛型接口,我们还可以创建泛型类。 注意,无法创建泛型枚举和泛型命名空间(?)

泛型类

  • 泛型类指的是实例部分的类型,所以类的静态属性不能使用这个泛型类型

泛型约束

1
2
3
4
5
6
7
8
interface Lengthwise {
length: number;
}

function loggingIdentity<T extends Lengthwise>(arg: T): T {
console.log(arg.length); // Now we know it has a .length property, so no more error
return arg;
}
  • 在泛型约束中使用类型参数(?)
  • 在泛型中使用类类型(?有点迷糊)

枚举

枚举

  • 数字枚举:不带初始化器的枚举应该被放在第一的位置,或者被放在使用了数字常量或其它常量初始化了的枚举后面
1
2
3
4
5
6
enum Direction {
Up,
Down,
Left,
Right
}
  • 字符串枚举:在一个字符串枚举里,每个成员都必须用字符串字面量,或另外一个字符串枚举成员进行初始化
  • 异构枚举:枚举可以混合字符串和数字成员,但不建议这样做;
  • 计算的和常量成员
  • 联合枚举与枚举成员的类型
    • 枚举类型本身变成了每个枚举成员的联合:可以避免一些低级错误
    • 枚举成员成为了类型
  • 运行时的枚举
  • 反向映射:从枚举值到枚举名字。不会为字符串枚举成员生成反向映射
1
2
3
4
5
enum Enum {
A
}
let a = Enum.A;
let nameOfA = Enum[a]; // "A"
  • cosnt 枚举: 常量枚举只能使用常量枚举表达式,并且不同于常规的枚举,它们在编译阶段会被删除。之所以可以这么做是因为,常量枚举不允许包含计算成员
1
2
3
4
const enum Enum {
A = 1,
B = A * 2
}
  • 外部枚举
1
2
3
4
5
declare enum Enum {
A = 1,
B,
C = 2
}

类型推论

基础

  • TypeScript 里,在有些没有明确指出类型的地方,类型推论会帮助提供类型
  • 这种推断发生在初始化变量和成员,设置默认参数值和决定函数返回值时

最佳通用类型

  • 当需要从几个表达式中推断类型时候,会使用这些表达式的类型来推断出一个最合适的通用类型

上下文类型

类型兼容性

介绍

  • TypeScript 里的类型兼容性是基于结构子类型的

基础

  • 基本规则:如果 x 要兼容 y,那么 y 至少具有与 x 相同的属性

比较两个函数

  • 比较参数和返回值

函数参数双向协变

可选参数及剩余参数

枚举

  • 枚举类型与数字类型兼容,并且数字类型与枚举类型兼容。不同枚举类型之间是不兼容的

  • 类与对象字面量和接口差不多,但有一点不同:类有静态部分和实例部分的类型。 比较两个类类型的对象时,只有实例的成员会被比较
  • 类的私有成员和受保护成员会影响兼容性

泛型

高级主题

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

高级类型

交叉类型(Intersection Types)

  • 交叉类型是将多个类型合并为一个类型,它包含了所需的所有类型的特性

联合类型(Union Types)

  • 联合类型表示一个值可以是几种类型之一
  • 如果一个值是联合类型,我们只能访问此联合类型的所有类型里共有的成员

类型保护与区分类型

  • 类型保护就是一些表达式,它们会在运行时检查以确保在某个作用域里的类型
  • 要定义一个类型保护,我们只要简单地定义一个函数,它的返回值是一个 类型谓词:pet is Fish 就是类型谓词。 谓词为 parameterName is Type 这种形式, parameterName 必须是来自于当前函数签名里的一个参数名
1
2
3
function isFish(pet: Fish | Bird): pet is Fish {
return (<Fish>pet).swim !== undefined;
}
  • typeof 类型保护
  • instanceof 类型保护

可以为 null 的类型

  • 默认情况下,类型检查器认为 null 与 undefined 可以赋值给任何类型
  • –strictNullChecks 标记可以解决此错误
  • 可选参数和可选属性:使用了 –strictNullChecks,可选参数会被自动地加上 | undefined(不会加 null 类型)
  • 类型保护和类型断言:如果编译器不能够去除 null 或 undefined,你可以使用类型断言手动去除。 语法是添加 !后缀: identifier!从 identifier 的类型里去除了 null 和 undefined

类型别名

  • 起别名不会新建一个类型,它创建了一个新名字来引用那个类型
1
2
3
4
5
6
7
8
9
10
type Name = string;
type NameResolver = () => string;
type NameOrResolver = Name | NameResolver;
function getName(n: NameOrResolver): Name {
if (typeof n === "string") {
return n;
} else {
return n();
}
}
  • 类型别名也可以是泛型
  • 可以使用类型别名来在属性里引用自己
  • 类型别名不能出现在声明右侧的任何地方(?)
  • 接口 vs. 类型别名:
    • 其一,接口创建了一个新的名字,可以在其它任何地方使用。 类型别名并不创建新名字—比如,错误信息就不会使用别名
    • 另一个重要区别是类型别名不能被 extends 和 implements(自己也不能 extends 和 implements 其它类型)
    • 另一方面,如果你无法通过接口来描述一个类型并且需要使用联合类型或元组类型,这时通常会使用类型别名(?不太理解)

字符串字面量类型

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

数字字面量类型

枚举成员类型

可辨识联合(Discriminated Unions)

  • 可以合并单例类型,联合类型,类型保护和类型别名来创建一个叫做 可辨识联合的高级模式,它也称做 标签联合或 代数数据类型。
赠人玫瑰,手有余香