typescript 学习 (1)

工作需要,开始学习 ts(只记点难的)。

基础类型

数组

1
2
let list1: number[] = [1, 2];
let list2: Array<number> = [1, 2];

元祖 Tuple

1
let x: [string, number] = ['hello', 10]; // OK

当访问越界的元素,使用联合类型替代:

1
2
x[3] = 'world'; // Ok , 可以是 string|number
x[6] = true; // Error

枚举

1
2
3
4
5
6
enum Color {
Red,
Green,
Blue,
};
let c: Color = Color.Green; // = 1

默认情况下,从 0 开始为元素编号。也可以手动指定起始、全部编号,也能用字符串等。

any

void

无返回值的函数

null 和 undefined

它们是所有类型的子类型。

类型断言

变量声明

解构

1
2
3
4
5
6
7
8
9
10
11
12
function f([first, second]: [number, number]) {
console.log(first);
console.log(second);
}
f(input);

let {a, b}: {a: string, b: number} = o;

type C = { a: string, b?: number }
function f({ a, b }: C): void {
// ...
}

接口

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
interface SquareConfig {
color?: string;
width?: number;
}

function createSquare(config: SquareConfig): {color: string; area: number} {
let newSquare = {color: "white", area: 100};
if (config.color) {
newSquare.color = config.color;
}
if (config.width) {
newSquare.area = config.width * config.width;
}
return newSquare;
}

let mySquare = createSquare({color: "black"});


// 只读属性
interface Point {
readonly x: number;
readonly y: number;
}

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

1
2
3
4
5
6
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!

上面代码的最后一行,可以看到就算把整个 ReadonlyArray 赋值到一个普通数组也是不可以的。 但是你可以用类型断言重写:

1
a = ro as number[];

额外的检查

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

function createSquare(config: SquareConfig): { color: string; area: number } {
// ...
}

// error: 'colour' not expected in type 'SquareConfig'
let mySquare = createSquare({ colour: "red", width: 100 });

// 绕开这些检查非常简单。 最简便的方法是使用类型断言:
let mySquare = createSquare({ width: 100, opacity: 0.5 } as SquareConfig);

然而,最佳的方式是能够添加一个字符串索引签名,前提是你能够确定这个对象可能具有某些做为特殊用途使用的额外属性。 如果 SquareConfig 带有上面定义的类型的 color 和 width 属性,并且还会带有任意数量的其它属性,那么我们可以这样定义它:

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

我们稍后会讲到索引签名,但在这我们要表示的是 SquareConfig 可以有任意数量的属性,并且只要它们不是 color 和 width,那么就无所谓它们的类型是什么。

还有最后一种跳过这些检查的方式,这可能会让你感到惊讶,它就是将这个对象赋值给一个另一个变量: 因为 squareOptions 不会经过额外属性检查,所以编译器不会报错。

1
2
let squareOptions = { colour: "red", width: 100 };
let mySquare = createSquare(squareOptions);

函数类型

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

可索引的类型

1
2
3
4
5
6
7
8
interface StringArray {
[index: number]: string;
}

let myArray: StringArray;
myArray = ["Bob", "Fred"];

let myStr: string = myArray[0];

你可以将索引签名设置为只读,这样就防止了给索引赋值:

1
2
3
4
5
interface ReadonlyStringArray {
readonly [index: number]: string;
}
let myArray: ReadonlyStringArray = ["Alice", "Bob"];
myArray[2] = "Mallory"; // error!

类类型

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) { }
}

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

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
interface ClockConstructor {
new (hour: number, minute: number): ClockInterface;
}
interface ClockInterface {
tick();
}

function createClock(ctor: ClockConstructor, hour: number, minute: number): ClockInterface {
return new ctor(hour, minute);
}

class DigitalClock implements ClockInterface {
constructor(h: number, m: number) { }
tick() {
console.log("beep beep");
}
}
class AnalogClock implements ClockInterface {
constructor(h: number, m: number) { }
tick() {
console.log("tick tock");
}
}

let digital = createClock(DigitalClock, 12, 17);
let analog = createClock(AnalogClock, 7, 32);

继承接口

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
interface Shape {
color: string;
}

interface PenStroke {
penWidth: number;
}

interface Square extends Shape, PenStroke {
sideLength: number;
}

let square = <Square>{};
square.color = "blue";
square.sideLength = 10;
square.penWidth = 5.0;

混合类型

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
12
13
14
15
16
17
18
19
20
class Control {
private state: any;
}

interface SelectableControl extends Control {
select(): void;
}

class Button extends Control implements SelectableControl {
select() { }
}

class TextBox extends Control {
select() { }
}

// 错误:“Image”类型缺少“state”属性。
class Image implements SelectableControl {
select() { }
}

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

默认为 public
在上面的例子里,我们可以自由的访问程序里定义的成员。 如果你对其它语言中的类比较了解,就会注意到我们在之前的代码里并没有使用 public 来做修饰;例如,C# 要求必须明确地使用 public 指定成员是可见的。 在 TypeScript 里,成员都默认为 public。

1
2
3
4
5
6
7
class Animal {
public name: string;
public constructor(theName: string) { this.name = theName; }
public move(distanceInMeters: number) {
console.log(`${this.name} moved ${distanceInMeters}m.`);
}
}

当成员被标记成 private 时,它就不能在声明它的类的外部访问。比如:

1
2
3
4
5
6
class Animal {
private name: string;
constructor(theName: string) { this.name = theName; }
}

new Animal("Cat").name; // 错误: 'name' 是私有的.

理解 protected

protected 修饰符与 private 修饰符的行为很相似,但有一点不同, protected 成员在派生类中仍然可以访问。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
class Person {
protected name: string;
constructor(name: string) { this.name = name; }
}

class Employee extends Person {
private department: string;

constructor(name: string, department: string) {
super(name)
this.department = department;
}

public getElevatorPitch() {
return `Hello, my name is ${this.name} and I work in ${this.department}.`;
}
}

let howard = new Employee("Howard", "Sales");
console.log(howard.getElevatorPitch());
console.log(howard.name); // 错误

注意,我们不能在 Person 类外使用 name,但是我们仍然可以通过 Employee 类的实例方法访问,因为 Employee 是由 Person 派生而来的。

构造函数也可以被标记成 protected。 这意味着这个类不能在包含它的类外被实例化,但是能被继承。比如,

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
class Person {
protected name: string;
protected constructor(theName: string) { this.name = theName; }
}

// Employee 能够继承 Person
class Employee extends Person {
private department: string;

constructor(name: string, department: string) {
super(name);
this.department = department;
}

public getElevatorPitch() {
return `Hello, my name is ${this.name} and I work in ${this.department}.`;
}
}

let howard = new Employee("Howard", "Sales");
let john = new Person("John"); // 错误: 'Person' 的构造函数是被保护的.

readonly 修饰符

你可以使用 readonly 关键字将属性设置为只读的。 只读属性必须在声明时或构造函数里被初始化。

1
2
3
4
5
6
7
8
9
class Octopus {
readonly name: string;
readonly numberOfLegs: number = 8;
constructor (theName: string) {
this.name = theName;
}
}
let dad = new Octopus("Man with the 8 strong legs");
dad.name = "Man with the 3-piece suit"; // 错误! name 是只读的.

参数属性

在 constructor 的参数前加上修饰符,能将修饰和赋值合并在一起:

1
2
3
4
5
6
7
8
9
10
11
12
13
class Octopus {
readonly numberOfLegs: number = 8;
constructor(readonly name: string) {
}
}
// 转化为:
var Octopus = /** @class */ (function () {
function Octopus(name) {
this.name = name;
this.numberOfLegs = 8;
}
return Octopus;
}());

存取器

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
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);
}

静态属性

使用 static 定义.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class Grid {
static origin = {x: 0, y: 0};
calculateDistanceFromOrigin(point: {x: number; y: number;}) {
let xDist = (point.x - Grid.origin.x);
let yDist = (point.y - Grid.origin.y);
return Math.sqrt(xDist * xDist + yDist * yDist) / this.scale;
}
constructor (public scale: number) { }
}

let grid1 = new Grid(1.0); // 1x scale
let grid2 = new Grid(5.0); // 5x scale

console.log(grid1.calculateDistanceFromOrigin({x: 10, y: 10}));
console.log(grid2.calculateDistanceFromOrigin({x: 10, y: 10}));

抽象类

抽象类做为其它派生类的基类使用。 它们一般不会直接被实例化。 不同于接口,抽象类可以包含成员的实现细节。 abstract 关键字是用于定义抽象类和在抽象类内部定义抽象方法。

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
abstract class Department {

constructor(public name: string) {
}

printName(): void {
console.log('Department name: ' + this.name);
}

abstract printMeeting(): void; // 必须在派生类中实现
}

class AccountingDepartment extends Department {

constructor() {
super('Accounting and Auditing'); // 在派生类的构造函数中必须调用 super()
}

printMeeting(): void {
console.log('The Accounting Department meets each Monday at 10am.');
}

generateReports(): void {
console.log('Generating accounting reports...');
}
}

let department: Department; // 允许创建一个对抽象类型的引用
department = new Department(); // 错误: 不能创建一个抽象类的实例
department = new AccountingDepartment(); // 允许对一个抽象子类进行实例化和赋值
department.printName();
department.printMeeting();
department.generateReports(); // 错误: 方法在声明的抽象类中不存在

高级技巧

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
class Greeter {
static standardGreeting = "Hello, there";
greeting: string;
greet() {
if (this.greeting) {
return "Hello, " + this.greeting;
}
else {
return Greeter.standardGreeting;
}
}
}

let greeter1: Greeter;
greeter1 = new Greeter();
console.log(greeter1.greet());

let greeterMaker: typeof Greeter = Greeter;
greeterMaker.standardGreeting = "Hey there!";

let greeter2: Greeter = new greeterMaker();
console.log(greeter2.greet());

在这里用了 typeof,它会取 Greeter 类的类型,而不是实例的类型。

把类当做接口使用

1
2
3
4
5
6
7
8
9
class Point {
x: number;
y: number;
}

interface Point3d extends Point {
z: number;
}
let point3d: Point3d = {x: 1, y: 2, z: 3};