TypeScript 概览

TypeScript

TypeScript 是 JavaScript 类型的超集,它可以编译成纯 JavaScript,可以在任何浏览器、任何计算机和任何操作系统上运行,并且是开源的。

基础类型

TypeScript 支持与 JavaScript 几乎相同的数据类型,此外还提供了方便使用的枚举类型。

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
// boolean
let isDone: boolean = false;
// number
// JavaScript 和 TypeScript 中的数字不是浮点类型 floating point 就是大整数类型 BigInteger
let decLiteral: number = 6;
let bigLiteral: number = 100n;
// string
let name: string = 'bob';
// Array
let list: number[] = [1,2,3];
let list: Array<number> = [1,2,3];
// Tuple
// 元组类型要求值类型、顺序和个数一一对应
let tuple: [string, string] = ['string', 'string'];
// enum
enum Color {Red = 0, Green, Blue}
let c: Color = Color.Green;
// Any
let source: any = 4
// Void
let unusable: void = undefined;
// Null & Undefined
let u: undefined = undefined;
let n: null = null;
// Never
function error(message: string): never {
throw new Error(message);
}
// 类型断言
// 明确知道值的类型时可使用类型断言
let strValue: any = 'str';
let length: number = (<string>strValue).length;
let length: number = (strValue as string).length

变量声明

  • var

    可以通过 var 关键字定义变量,但是 var 声明可以在包含它的函数、模块、命名空间或全局作用域内部任何位置被访问,包含它的代码块对此没有影响,这可能会引发一些错误,比如多次声明同一个变量并不会报错。

  • let

    当用 let 声明一个变量,它使用的是词法作用域或块作用域。不同于使用 var 声明的变量那样可以在包含它们的函数外访问,块作用域变量在包含它们的块之外是不能访问的。

  • const

    const 拥有与 let 相同的作用域规则,但是声明的变量被赋值后不能再改变。

每次进入一个作用域时,它创建了一个变量的环境,就算作用域内代码已经执行完毕,这个环境与其捕获的变量依然存在。当 let 声明出现在循环体里时拥有完全不同的行为,不仅是在循环里引入了一个新的变量环境,而是针对每次迭代都会创建这样一个新作用域。

接口

TypeScript 的核心原则之一是对值所具有的结构进行类型检查。 它有时被称做 “鸭式辨型法”或”结构性子类型化”。 在 TypeScript 里,接口的作用就是为这些类型命名和为你的代码或第三方代码定义契约。

JavaScript 为开发者提供了非常大的灵活性:比如你可以为一个初始化为整数的变量赋值一个函数。但同时这种不确定性也会带来很多的麻烦,TypeScript 内置的接口就可以用来解决这个问题。

接口帮助我们在赋值和传递参数时进行类型检查,确保我们给变量的赋值符合变量的类型或接收参数正确(接口在某种程度上表示了变量或函数对于赋值给它的值或传递给它的参数的一种期望和要求)。

可选属性

接口里的属性不全都是必需的,有些是只在某些条件下存在,或者根本不存在,可以为接口定义可选属性来实现这些需求。

1
2
3
4
interface OptionAttribute {
optionNum?: number;
optionStr?: string;
}

可选属性的好处之一是可以对可能存在的属性进行预定义,好处之二是可以捕获引用了不存在的属性时的错误。

只读属性

如果希望对象的某些属性只有在刚创建的时候修改其值,可以通过在属性名前用 readonly 来指定属性为只读属性。

1
2
3
4
interface ReadonlyAttribute {
readonly num: number;
readonly str: string;
}

TypeScript 具有 ReadonlyArray<T> 类型,它与 Array<T> 类似,只是把所有可变方法去掉了,因此可以保证数组创建后再也不能被修改。

readonly vs const

使用 const 来修饰变量,使用 readonly 来修饰属性

额外的类型检查

TypeScript 中,对象字面量赋值给变量或作为参数进行传递的时候,会经过 “额外属性检查”,如果一个对象字面量存在任何 “目标类型” 不包含的属性时,会发生错误。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
interface SquareConfig { 
width: number;
color?: string;
}

function createSquare(config: SquareConfig): { color: string, area: number } {
let newSquare = { color: 'blue', area: 0 };
if (config.width) newSquare.area = config.width * config.width;
if (config.color) newSquare.color = config.color;
return newSquare;
}

// error: height not expected in type SquareConfig
let mySquare = createSquare({ color: 'green', width: 10, height: 10 });

使用 ‘类型断言’ 可以绕开额外属性检查

1
let mySquare = createSquare({ color: 'green', width: 10, height: 10 } as SquareConfig);

但是更好的做法是在接口中提供一个字符串索引签名

1
2
3
4
5
interface SquareConfig { 
width: number;
color?: string;
[propName: string]: any;
}

函数类型

接口能够描述 JavaScript 中对象拥有的各种各样的外形。 除了描述带有属性的普通对象外,接口也可以描述函数类型。

为了使用接口表示函数类型,我们需要给接口定义一个调用签名。 它就像是一个只有参数列表和返回值类型的函数定义。

参数列表里的每个参数都需要名字和类型。

1
2
3
interface TypeFunc {
(source: string, subString: string): boolean;
}

这样定义后,我们可以像使用其它接口一样使用这个函数类型的接口。 下例展示了如何创建一个函数类型的变量,并将一个同类型的函数赋值给这个变量。

1
2
3
4
5
let mySearchFunc: TypeFunc;
mySearchFunc = function(source: string, subString: string) {
let result = source.search(subString);
return result > -1;
}

对于函数类型的类型检查来说,函数的参数名不需要与接口定义里的名字相匹配。

函数的参数会逐个进行检查,要求对应位置上的参数类型是兼容的。如果没有指定类型,TypeScript 的类型系统会推断出参数类型,而函数的返回值类型是通过其返回值推断出来的。

可索引的类型

与使用接口描述函数类型差不多,我们也可以描述那些能够通过索引得到的类型。可索引类型具有一个索引签名,它描述了对象索引的类型,还有相应的索引返回值类型。

1
2
3
4
5
6
7
8
// 表明了当用 number 去索引 StringArr 时会得到 string 类型的返回值
interface StringArr {
[index: number]: string;
}

let myArr: StringArr;
myArr = ["A","B"];
let myStr: string = myArr[0];

索引签名共有两种类型:字符串和数字。可以同时使用这两种类型的索引,但是数字索引的返回值必须是字符串索引返回值类型的子类型。这是因为当使用 number 来索引时,TypeScript 会将它转换为 string 然后再去索引对象。

1
2
3
4
interface StringArray { 
[index: string]: string;
[index: number]: string;
}

一般将索引签名设置为只读,这样可以防止给索引赋值。

类类型

TypeScript 中的接口也能够用来明确的强制一个类去符合某种协议/契约。

1
2
3
4
5
6
7
8
9
10
11
12
interface ClockInterface {
currentTime: Date;
setTime:(d: Date);
}

class Clock implements ClockInterface {
currentTime: Date;
setTime:(d: Date) {
this.currentTime = d;
}
constructor(h: number, m: number) {}
}

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

类静态部分与实例部分的区别

类具有两个类型 :静态部分的类型和实例的类型。

当一个类实现了一个接口时,只对其实例部分进行类型检查。constructor 存在于类的静态部分,不在检查范围内。因此我们应该直接操作类的静态部分。

可以认为类的静态部分指的是类本身,实例部分指的是类实例化出来的对象。

继承接口

和类一样,接口也可以相互继承。这让我们能够从一个接口里复制成员到另一个接口里,可以更灵活地将接口分割到可重用的模块里。

1
2
3
4
5
6
7
8
9
10
11
interface Father {
familyName: string;
}

interface Son extends Father {
givenName: string;
}

let aPerson = <Son>{};
aPerson.familyName = 'S';
aPerson.givenName = 'T';

一个接口可以继承多个接口,创建出多个接口的合成接口。

1
2
3
4
5
6
7
8
9
10
11
interface Shape {
color: string;
}

interface Stroke {
penWidth: number;
}

interface Square extends Shape, Stroke {
width: number;
}

混合类型

接口能够描述 JavaScript 中丰富的类型,可以使用 混合类型 来使某个对象具有多个类型。

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

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

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

接口继承类

当接口继承了一个类类型时,它会继承类的成员但不继承其实现。就好像接口声明了所有类中存在的成员,但没有提供具体实现一样。接口同样会继承到类的 privateprotected 成员。这意味着当你创建了一个接口继承了一个拥有私有或受保护的成员的类时,这个接口类型只能被这个类或其子类所实现。

当你有一个庞大的继承结构时这很有用,但要指出的是你的代码只在子类拥有特定的属性时起作用。

ECMAScript 2015 , 也就是 ECMAScript 6 开始, JavaScript 中也能使用基于类的面向对象的方式。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 定义一个类
class Person {
familyName: string;
constructor(familyName: string, givenName: string, age: number) {
this.familyName = familyName;
}
say() {
return 'Hello, my name is ' + this.familyName + ' ' + this.givenName;
}
move(distanceInMeters = 0) {
console.log('${this.name} moved ${distanceInMeters}m.');
}
}
// 创建类实例
lei aPerson = new Person('Wang', 'XiaoHong', 22);

在引用任何一个类的成员时都需要使用 this ,它表示我们访问的是类的成员。

继承

TypeScript 中允许使用继承来扩展现有的类。

1
2
3
4
5
6
7
8
class Student extends Person {
constructor(familyName: string, givenName: string, age: number) {
super(familyName, givenName, age);
}
move(distanceInMeters = 5) {
super.move(distanceInMeters);
}
}

如果子类包含了构造函数,它必须调用 super() ,它会执行基类的构造函数。在构造函数中访问 this 的属性之前,一定要先调用 super()

子类从父类中继承属性和方法

修饰符

  • public

    publicTypeScript 中类成员的默认属性

  • private

    类成员声明为 private 时,不能在生声明它的类的外部访问

  • protected

    类成员声明为 protected 时,在派生类中仍可访问

    构造函数也能被标记为 protected ,这意味着它不能在包含它的类外被实例化,但是能被继承

  • readonly

    通过 readonly 属性将属性设置为只读,只读属性必须在声明时或构造函数里被初始化

参数属性

参数属性通过给构造函数参数前添加一个访问限定符来声明。

1
2
3
4
class Student {
constructor(public familyName: string, public givenName: string, public age: number){
}
}

通过这个特性我们可以方便的在一个地方定义并初始化一个成员。

存取器

TypeScript 支持通过 getters/setters 来截取对对象成员的访问,它们可以有效的控制对对象成员的访问。

1
2
3
4
5
6
7
8
9
10
11
class student {
private _familyName: string;

get familyName(): string {
return this._familyNmae;
}

set familyName(fName: string) {
this._familyName = fName;
}
}

需要注意的是,存取器只支持 ECMAScript 5 或更高,其次,只带有 getter 存取器被推断为 readonly

静态属性

类的实例成员是那些仅当类被实例化时才会被初始化的属性,而类的静态成员存在于类本身上面而不是类的实例上。实例想要访问静态属性时,需要在其加上类名。

1
2
3
4
5
6
7
class student {
static school = 'ts';
...
getStudentSchool() {
return student.school;
}
}

抽象类

抽象类一般作为其它派生类的基类使用,一般不会被实例化。不同于接口,抽象类可以包含成员的实现细节。

使用 abstract 关键字定义抽象类和在抽象类中定义抽象方法。

1
2
3
abstract class Human {
abstract function_name: return_type;
}

抽象类中的抽象方法不包含具体实现并且必须在派生类中实现。

抽象方法的语法与接口语法相似,两者都是定义方法签名但不包含方法体。然而,抽象方法必须包含 abstract 关键字并且可以包含访问修饰符。

函数

函数是 JavaScript 应用程序的基础。 它帮助你实现抽象层,模拟类,信息隐藏和模块。 在 TypeScript 里,虽然已经支持类,命名空间和模块,但函数仍然是主要的定义行为的地方。 TypeScriptJavaScript 函数添加了额外的功能,让我们可以更容易地使用。

函数类型

1
2
3
function fun(arg_1: arg_type, arg_2: arg_type, ...): return_type {
...
}

TypeScript 能够通过返回语句自动推断出返回值类型,因此函数返回类型一般是省略的。

1
let fun_var_name: (arg_1: arg_type, arg_2: arg_type, ...) => return_type;

函数类型包括两部分,参数类型和返回值类型。而且只要参数类型是匹配的,就认为它是有效的函数类型,而不在乎参数名是否准确。

可选参数和默认参数

JavaScript 中函数的每个参数都是可选的,可传可不传,没有传参的时候它的值就是 undefined。而 TypeScript 中函数的每个参数默认情况下都是必须的,编译器检查用户是否为每个参数都传入了值,还会假设只有这些参数被传递进函数-即传递给一个函数的参数个数必须与函数期望的参数个数一致。

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

const sum = myAdd(1); // error: Expected 2 arugements, but got 1
const sum1 = myAdd(1, 2, 3); // error: Expected 2 arugements, but got 3

TypeScript 中,可以在参数名称旁使用 ? 实现可选参数的功能。

1
2
3
4
5
6
7
// 可选参数
function buildStudent(name: string, grade: number, gender?: string) {
return name + ' ' + grade + ' ' + (gender ? gender : '');
}

const student_1 = buildStudent('Tom', 2);
const student_2 = buildStudent('Jerry', 2, 'male');

需要注意的是,可选参数必须跟在可选参数后面。

TypeScript 中,当用户没有传递这个参数或传递的值是 undefined 时,我们可以为参数提供一个默认值。在所有必须参数后面的带默认初始化的参数都是可选的,与可选参数一样,在调用函数的时候可以省略-即可选参数与末尾的默认参数共享参数类型。

需要注意的是,如果带默认值的参数出现在必须参数之前,用户必须明确的传入 undefined 值来获得默认值。

剩余参数

如果你想同时操作多个参数或不知道会有多少个参数传递进来,在 JavaScript 中,你可以通过 arguments 来访问所有传入的参数,在 TypeScript 中,你则可以将所有参数收集到一个变量里。

1
2
3
4
5
6
// 剩余参数
function buildChildren(name: string, age: number, ...restOfProperty: string[]) {
return `${name} ${age} ${restOfProperty.join(' ')}`;
}

const children = buildChildren('Mike', 12, 'male', 'grade 3');

泛型

泛型是程序设计语言的一种特性,是程序员在编写代码时定义的一些可变部分,这些部分在使用前必须做出指明。

1
2
3
4
5
6
7
// 类型变量 T 会捕获用户传入的类型,之后我们就可以使用者个类型
function identity<T>(arg: T): T {
return arg;
}

const strId = identity('JavaScript');
const numId = identity(12);

泛型变量

使用泛型函数时,编译器要求你在函数体必须正确的使用这个通用的类型-即你必须把这些参数当做是任意或所有类型。

泛型函数

1
2
3
4
5
6
7
function combineFunc<T,K>(x: T, y: K): [T, K] {
return [T, K];
}

let combine: <T,K>(x: T, y: K) => [T,K] = combineFunc;
// or
let combine: { <T,K>(x: T, y: K): [T, K] } = combineFunc;

泛型接口

1
2
3
4
5
interface CombineFunc {
<T,K>(x: T, y: K): [T, K];
}

let combine: CombineFunc = (x, y) => [x, y];

泛型类

1
2
3
4
5
6
class CombineFunC {
combine: <T,K>(x: T, y: K) => [T, K];
}

let combineC: CombineFunC = new CombineFunC<number, number>();
combineC.combine = (x, y) => [x, y];

枚举

使用枚举可以定义一些带名字的常量,清晰的表达意图或创建一组有区别的用例。TypeScript 支持数字的和基于字符串的枚举。

数字枚举

数字枚举默认的枚举值是从 0 开始自增长的,如果你为第一个枚举名字指定了一个数字枚举值,则会从这个数字值开始自增长。

1
2
3
4
5
6
enum Status {
DELETED = -1,
VALID,
UNPAID,
PAID
}

字符串枚举

字符串枚举中,每个成员都需要使用字符串字面量或另外一个字符串枚举成员进行初始化。

需要注意的是,字符串枚举没有自增长行为。

1
2
3
4
5
6
enum StatusTip {
DELETED = 'Order has been deleted.',
VALID = 'Order is valid.',
UNPAID = 'Order is unpaid.',
PAID = 'Order is paid'
}

异构枚举

枚举可以混合字符串和数字成员。

1
2
3
4
enum HeterogeneousEnum {
NO = 0,
YES = 'Yes'
}

计算的和常量成员

每个枚举成员都带有一个值,它可以是常量或计算出来的。当满足如下条件时,枚举成员被当做常量:

  • 它是枚举的第一个成员,且没有初始化器,此时它的值为 0

  • 它不带有初始化器且它之前的枚举成员是一个数字常量,此时它的值为上一个枚举成员的值 + 1

  • 枚举成员使用常量枚举表达式初始化。常量枚举表达式是 TypeScript 表达式的子集,它可以在编译阶段求值。当一个表达式满足以下条件时,它就是一个常量枚举表达式:

    • 一个枚举表达式字面量

    • 一个对之前定义的常量枚举成员的引用

    • 带括号的常量枚举表达式

    • 一元运算符 + - ~ 其中之一应用在了常量枚举表达式

    • 常量枚举表达式作为二元运算符 +, -, *, /, %, <<, >>, >>>, &, |, ^ 的操作对象(若常量枚举表达式求值后为 NaNInfinity,则会在编译时报错)

      1
      2
      3
      4
      5
      6
      7
      enum FileAccess {
      None,
      Read = 1 << 1,
      Write = 1 << 2,
      ReadWrite = Read | Write,
      G = '1024'.length
      }

联合枚举与枚举成员的类型

存在一种特殊的非计算的常量枚举成员的子集:字面量枚举成员。字面量枚举成员是指不带有初始值的常量枚举成员或是被初始化为

  • 任何字符串字面量
  • 任何数字字面量
  • 应用了一元 - 符号的数字字面量

当所有枚举成员都拥有字面量枚举值时,它就带有了一种特殊的语义。

运行时枚举

枚举是在运行时真正存在的对象

反向映射

数字枚举成员具有从枚举值到枚举名字的反向映射。

1
2
3
4
5
6
7
8
enum Status {
DELETED = -1,
VALID,
UNPAID,
PAID
}

console.log(Status[0]); // VALID

const 枚举

常量枚举只能使用常量枚举表达式,并且不同于常规的枚举,它们在编译阶段会被删除-常量枚举成员会在使用的地方被内联进来,这是因为常量枚举不允许包含计算成员。

1
2
3
4
5
6
7
8
const enum Directions {
Up,
Down,
Left,
Right
}

let directions = [ Directions.Up, Directions.Down, Directions.Left, Directions.Right ];

类型推论

类型推论是指能够在编译期间自动推导出值的类型的能力。一般发生在初始化变量和成员,设置默认参数值和决定函数返回值时。

最佳通用类型

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

上下文类型

当表达式的类型与所处的位置相关时,类型推论按照上下文归类来推论类型。当然,如果表达式包含了明确的类型信息,上下文的类型会被忽略。

类型兼容性

TypeScript 中的类型兼容性是基于结构子类型的-结构子类型是一种只使用其成员来描述类型的方式。

原始类型和对象类型的兼容性

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

1
2
3
4
5
// 如果 x 要兼容 y, y 至少要与 x 拥有相同的属性
let x = { name: 'x' };
let y = { name: 'y', alias: 'Y' };
x = y;
y = x; // Property 'alias' is missing in type '{ name: string; }' but required in type '{ name: string; alias: string; }

函数的类型兼容性

如果函数 x 要兼容函数 y,那么 x 至少要与 y 具有相同的参数并且返回值类型相同。

1
2
3
4
5
6
7
8
let funCom = (x: number, y: number) => x + y;
let funAbc = (x: number, y: number) => [x, y];
let funSub = (x: number) => x;
let funDef = (x: number, y: number) => x * y;
funCom = funSub;
funSub = funCom; // Type '(x: number, y: number) => number' is not assignable to type '(x: number) => number'
funCom = funAbc; // Type '(x: number, y: number) => number[]' is not assignable to type '(x: number, y: number) => number'.Type 'number[]' is not assignable to type 'number'
funCom = funDef;

类的兼容性

比较两个类类型的对象时,只有实例的成员会被比较。 静态成员和构造函数不在比较的范围内。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class W {
static sign: string = 'AAAAA';
pro: string = 'W';
constructor(name: string){}
}

class Y {
pro: string = 'W';
constructor(length: number) {}
}

let wC = new W('W');
let yC = new Y(2);

wC = yC; // OK
yC = wC; // OK

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

泛型的兼容性

1
2
3
4
5
6
interface NotEmpty<T> {
}

let gA: NotEmpty<number>;
let gB: NotEmpty<string>;
gA = gB; // ok
1
2
3
4
5
6
7
interface NotEmpty<T> {
data: T;
}

let gA: NotEmpty<number>;
let gB: NotEmpty<string>;
gA = gB; // Error

高级类型

交叉类型

交叉类型是将多个类型合并为一个类型。 这让我们可以把现有的多种类型叠加到一起成为一种类型,它包含了所需的所有类型的特性。

联合类型

联合类型表示一个值可以是几种类型之一。

1
2
3
4
5
6
7
8
/ 联合类型
// 联合类型表示一个值可以是几个类型之一
function pad(padding: number | string) {
return padding;
}

console.log(pad(1));
console.log(pad('A'));

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
interface Elephant {
name: string;
walk();
}

interface Shark {
name: string;
swim();
}

function getAnimal(): Elephant | Shark {
return { name: 'Elephant', walk() { console.log('walk') } } as Elephant;
}

let aAnimal = getAnimal();
console.log(aAnimal.name);
aAnimal.walk();
aAnimal.swim();
作者

Y2hlbmdsZWk=

发布于

2019-09-13

更新于

2021-09-01

许可协议