Yapei Li

专注于前端领域

0%

TypeScript

使用双重断言,先断言成兼容所有类型的 any,再断言为别的类型可以避免编译器将不会报错:const element = (event as any) as HTMLElement

nullundefined是所有类型的子类型。 就是说可以把 nullundefined赋值给number类型的变量

类型检查器不会去检查属性的顺序,只要相应的属性存在并且类型也是对的就可以

对象字面量会被特殊对待而且会经过 额外属性检查,当将它们赋值给变量或作为参数传递的时候。 如果一个对象字面量存在任何“目标类型”不包含的属性时,会得到一个错误

TypeScript支持两种索引签名:字符串和数字

当使用 number来索引时,JavaScript会将它转换成string然后再去索引对象。也就是用 100(一个number)去索引等同于使用”100”(一个string)去索引,因此两者需要保持一致

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

类类型的只是很重要

默认为 public 类的外部可访问

private类的外部不能访问

protected 在派生类(继承类)内部可以访问

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

返回值类型是函数类型的必要部分,如果函数没有返回任何值,也必须指定返回值类型为 void而不能不写。

剩余参数会被当做个数不限的可选参数。 可以一个都没有,同样也可以有任意个;编译器创建参数数组,名字是在省略号(…)后面给定的名字,可以在函数体内使用这个数组

泛型:在调用函数时确定入参与出参的类型,达到返回值的类型与传入参数的类型相同,的目的

绕开额外的属性检查:

1
2
3
4
5
interface SquareConfig {
color?: string;
width?: number;
[propName: string]: any; //会带有任意数量的其它属性[propName: string]: any;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
//接口
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"});
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// 泛型定义
function identity<T>(arg: T): T {
return arg;
}

//泛型使用 确定T类型
let output = identity<string>("myString"); // type of output will be 'string'
let output = identity("myString"); // type of output will be 'string'


//泛型函数loggingIdentity,接收类型参数T和参数arg,参数arg是类型是T的数组,并返回元素也是类型是T的数组
function loggingIdentity<T>(arg: T[]): T[] {
console.log(arg.length); // Array has a .length, so no more error
return arg;
}

//另一种实现
function loggingIdentity<T>(arg: Array<T>): Array<T> {
console.log(arg.length); // Array has a .length, so no more error
return arg;
}

1
2
3
4
5
6
7
8
9
10
11
12
//函数类型 接口
interface SearchFunc {
//入参: 返回值
(source: string, subString: string): boolean;
}

//对于函数类型的类型检查来说,函数的参数名不需要与接口里定义的名字相匹配
let mySearch: SearchFunc;
mySearch = function(src: string, sub: string): boolean {
let result = src.search(sub);
return result > -1;
}
1
2
3
4
5
6
7
8
9
// 断言
interface Foo {
bar: number;
bas: string;
}

const foo = {} as Foo;
foo.bar = 123;
foo.bas = 'hello';

基础类型

布尔值、数字、字符串

1
2
3
4
5
6
7
8
9
10
11
12
13
14
//布尔值
let isDone: boolean = false;
//数字
let decLiteral: number = 6;
let hexLiteral: number = 0xf00d;
let binaryLiteral: number = 0b1010;
let octalLiteral: number = 0o744;
//字符串
let name: string = "bob";
name = "smith";

let name: string = `Gene`;
let age: number = 37;
let sentence: string = `Hello, my name is ${ name }`

数组

1
2
3
4
5
//元素类型后面接上 []
let list: number[] = [1, 2, 3];

//使用数组泛型,Array<元素类型>
let list: Array<number> = [1, 2, 3];

元组 Tuple

允许表示一个已知元素数量和类型的数组,各元素的类型不必相同

1
2
3
4
5
let x: [string, number];
// Initialize it
x = ['hello', 10]; // OK
// Initialize it incorrectly
x = [10, 'hello']; // Error

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

1
2
3
4
5
x[3] = 'world'; // OK, 字符串可以赋值给(string | number)类型

console.log(x[5].toString()); // OK, 'string' 和 'number' 都有 toString

x[6] = true; // Error, 布尔不是(string | number)类型

枚举 enum

使用枚举类型可以为一组数值赋予友好的名字。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
enum Color {Red, Green, Blue}
let c: Color = Color.Green; //1

//默认情况下,从0开始为元素编号。 你也可以手动的指定成员的数值
enum Color {Red = 1, Green, Blue}
let c: Color = Color.Green; //2

//全部都采用手动赋值
enum Color {Red = 1, Green = 2, Blue = 4}
let c: Color = Color.Green; //2

//知道数值为2,但是不确定它映射到Color里的哪个名字,可以查找相应的名字
enum Color {Red = 1, Green, Blue}
let colorName: string = Color[2];

console.log(colorName); // 显示'Green'因为上面代码里它的值是2

任何类型 Any

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
//在对现有代码进行改写的时候,any类型是十分有用的
let notSure: any = 4;
notSure = "maybe a string instead";
notSure = false; // okay, definitely a boolean

// Object类型的变量只是允许你给它赋任意值 - 但是却不能够在它上面调用任意的方法,即便它真的有这些方法
let notSure: any = 4;
notSure.ifItExists(); // okay, ifItExists might exist at runtime
notSure.toFixed(); // okay, toFixed exists (but the compiler doesn't check)

let prettySure: Object = 4;
prettySure.toFixed(); // Error: Property 'toFixed' doesn't exist on type 'Object'.

//只知道一部分数据的类型时,any类型也是有用的
let list: any[] = [1, true, "free"];
list[1] = 100;

Void 当一个函数没有返回值时

1
2
3
4
5
function warnUser(): void {
console.log("This is my warning message");
}
//声明一个void类型的变量没有什么大用,因为你只能为它赋予undefined和null
let unusable: void = undefined;

Null Undefined

nullundefined是所有类型的子类型。 就是说可以把 nullundefined赋值给number类型的变量

当指定了--strictNullChecks标记,nullundefined只能赋值给void它们各自

1
2
3
//undefined和null两者各自有自己的类型分别叫做undefined和null
let u: undefined = undefined;
let n: null = null;

never 永不存在的值的类型

never类型是那些总是会抛出异常或根本就不会有返回值的函数表达式或箭头函数表达式的返回值类型

never类型是任何类型的子类型,也可以赋值给任何类型;然而,没有类型是never的子类型或可以赋值给never类型(除了never本身之外)。 即使 any也不可以赋值给never

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 返回never的函数必须存在无法达到的终点
function error(message: string): never {
throw new Error(message);
}

// 推断的返回值类型为never
function fail() {
return error("Something failed");
}

// 返回never的函数必须存在无法达到的终点 死循环
function infiniteLoop(): never {
while (true) {
}
}

Object

object表示非原始类型,也就是除numberstringbooleansymbolnullundefined之外的类型

1
2
3
4
5
6
7
8
9
10
//使用object类型,就可以更好的表示像Object.create这样的API
declare function create(o: object | null): void;

create({ prop: 0 }); // OK
create(null); // OK

create(42); // Error
create("string"); // Error
create(false); // Error
create(undefined); // Error

类型断言

TypeScript 允许你覆盖它的推断,并且能以你任何你想要的方式分析它,这种机制被称为「类型断言」

断言能让你更容易的从遗留项目中迁移

类型断言的一个常见用例是当从 JavaScript 迁移到 TypeScript 时:

1
2
3
const foo = {};
foo.bar = 123; // Error: 'bar' 属性不存在于 ‘{}’
foo.bas = 'hello'; // Error: 'bas' 属性不存在于 '{}'

发出了错误警告,因为foo的类型推断为 {},即具有零属性的对象。因此,不能在它的属性上添加 bar bas,可以通过类型断言来避免此问题:

1
2
3
4
5
6
7
8
interface Foo {
bar: number;
bas: string;
}

const foo = {} as Foo;
foo.bar = 123;
foo.bas = 'hello';

as foo<foo>

1
2
let foo: any;
let bar = <string>foo; // 现在 bar 的类型是 'string'

JSX 中使用 <foo> 的断言语法时,这会与JSX的语法存在歧义,因此,为了一致性,建议使用 as foo 的语法来为类型断言

双重断言

1
2
3
function handler(event: Event) {
const mouseEvent = event as MouseEvent;
}

然而,如下例子中的代码将会报错,尽管使用者已经使用了类型断言:

1
2
3
function handler(event: Event) {
const element = event as HTMLElement; // Error: 'Event' 和 'HTMLElement' 中的任何一个都不能赋值给另外一个
}

可以使用双重断言。首先断言成兼容所有类型的 any,编译器将不会报错:

1
2
3
function handler(event: Event) {
const element = (event as any) as HTMLElement; // ok
}

TypeScript 是怎么确定单个断言是否足够

S 类型是 T 类型的子集,或者 T 类型是 S 类型的子集时,S 能被成功断言成 T

解构

解构数组

1
2
3
4
5
6
7
//作用于函数参数:
let input = [1, 2];
function f([first, second]: [number, number]) {
console.log(first);
console.log(second);
}
f(input);

对象解构

属性重命名

1
2
3
4
5
6
7
8
let o = {
a: "foo",
b: 12,
c: "bar"
};
let { a: newName1, b: newName2 } = o;
//冒号不是指示类型的。 如果你想指定它的类型, 仍然需要在其后写上完整的模式
let {a, b}: {a: string, b: number} = o;

默认值

1
2
3
function keepWholeObject(wholeObject: { a: string, b?: number }) {
let { a, b = 1001 } = wholeObject;
}

接口 interface

TypeScript的核心原则之一是对值所具有的结构进行类型检查。

接口的作用就是为这些类型命名和为你的代码或第三方代码定义契约

使用接口来描述:必须包含一个label属性且类型为string:

1
2
3
4
5
6
7
8
9
10
interface LabelledValue {
label: string;
}

function printLabel(labelledObj: LabelledValue) {
console.log(labelledObj.label);
}

let myObj = {size: 10, label: "Size 10 Object"};
printLabel(myObj);

LabelledValue接口代表了有一个 label属性且类型为string的对象

只要传入的对象满足上面提到的必要条件,那么它就是被允许的。

还有一点值得提的是,类型检查器不会去检查属性的顺序,只要相应的属性存在并且类型也是对的就可以

传入的对象参数实际上会包含很多属性,但是编译器只会检查那些必需的属性是否存在,并且其类型是否匹配

可选属性

接口里的属性不全都是必需的。 有些是只在某些条件下存在,或者根本不存在。

带有可选属性的接口与普通的接口定义差不多,只是在可选属性名字定义的后面加一个?符号

在应用“option bags”模式时很常用,即给函数传入的参数对象中只有部分属性赋值了:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
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"});

好处

1、可以对可能存在的属性进行预定义
2、可以捕获引用了不存在的属性时的错误

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
//故意将 createSquare里的color属性名拼错,就会得到一个错误提示
interface SquareConfig {
color?: string;
width?: number;
}

function createSquare(config: SquareConfig): { color: string; area: number } {
let newSquare = {color: "white", area: 100};
if (config.clor) {
// Error: Property 'clor' does not exist on type 'SquareConfig'
newSquare.color = config.clor;
}
if (config.width) {
newSquare.area = config.width * config.width;
}
return newSquare;
}

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

只读属性

一些对象属性只能在对象刚刚创建的时候修改其值。 在属性名前用 readonly来指定只读属性:

1
2
3
4
interface Point {
readonly x: number;
readonly y: number;
}

通过赋值一个对象字面量来构造一个Point。 赋值后, x和y再也不能被改变了

1
2
let p1: Point = { x: 10, y: 20 };
p1.x = 5; // error!

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

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[];

readonly vs const

做为变量使用的话用 const,若做为属性则使用readonly

额外的属性检查

对象字面量会被特殊对待而且会经过 额外属性检查,当将它们赋值给变量作为参数传递的时候。 如果一个对象字面量存在任何“目标类型”不包含的属性时,会得到一个错误。

1
2
3
4
5
6
7
8
9
10
11
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 });

绕开额外的属性检查

1、使用类型断言

1
let mySquare = createSquare({ width: 100, opacity: 0.5 } as SquareConfig);

2、(最佳的方式)添加一个字符串索引签名

SquareConfig带有上面定义的类型的color和width属性,并且还会带有任意数量的其它属性[propName: string]: any;

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

3、将这个对象赋值给一个另一个变量: 因为 squareOptions不会经过额外属性检查,所以编译器不会报错。

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

对于包含方法和内部状态的复杂对象字面量来讲,可能需要使用这些技巧,但是大部额外属性检查错误是真正的bug。 就是说遇到了额外类型检查出的错误,比如“option bags”,应该去审查一下类型声明

函数类型

接口也可以描述函数类型,需要给接口定义一个调用签名。 就像是一个只有参数列表返回值类型的函数定义。参数列表里的每个参数都需要名字和类型。

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

何创建一个函数类型的变量,并将一个同类型的函数赋值给这个变量:

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

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

1
2
3
4
5
let mySearch: SearchFunc;
mySearch = function(src: string, sub: string): boolean {
let result = src.search(sub);
return result > -1;
}

可索引的类型

接口可以描述那些能够“通过索引得到”的类型,比如a[10]ageMap["daniel"]可索引类型具有一个 索引签名,它描述了对象索引的类型,还有相应的索引返回值类型

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

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

let myStr: string = myArray[0];

TypeScript支持两种索引签名:字符串数字

可以同时使用两种类型的索引,但是数字索引返回值必须是字符串索引返回值类型的子类型

因为当使用 number来索引时,JavaScript会将它转换成string然后再去索引对象。 也就是说用 100(一个number)去索引等同于使用"100"(一个string)去索引,因此两者需要保持一致

1
2
3
4
5
6
7
8
9
10
11
12
class Animal {
name: string;
}
class Dog extends Animal {
breed: string;
}

// 错误:使用数值型的字符串索引,有时会得到完全不同的Animal!
interface NotOkay {
[x: number]: Animal;
[x: string]: Dog;
}

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

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

类类型(先定义 后实现)

实现的时候 必须 先要通过一个类来实现(implements)这个接口(interface )然后在这个类里边进行实现

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

属性的定义和实现 implements

1
2
3
4
5
6
7
8
interface ClockInterface {
currentTime: Date;
}

class Clock implements ClockInterface {
currentTime: Date;
constructor(h: number, m: number) { }
}

方法的定义和实现 implements

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
interface Shape {
color: string;
}

interface Square extends Shape {
sideLength: number;
}

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

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

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;

在使用JavaScript第三方库的时候,可能需要像上面那样去完整地定义类型

接口继承类

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");

继承

简单示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class Animal {
move(distanceInMeters: number = 0) {
console.log(`Animal moved ${distanceInMeters}m.`);
}
}

class Dog extends Animal {
bark() {
console.log('Woof! Woof!');
}
}

const dog = new Dog();
dog.bark();
dog.move(10);
dog.bark();

复杂的例子

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
class Animal {
name: string;
constructor(theName: string) { this.name = theName; }
move(distanceInMeters: number = 0) {
console.log(`${this.name} moved ${distanceInMeters}m.`);
}
}

class Snake extends Animal {
constructor(name: string) { super(name); }
move(distanceInMeters = 5) {
console.log("Slithering...");
super.move(distanceInMeters);
}
}

class Horse extends Animal {
constructor(name: string) { super(name); }
move(distanceInMeters = 45) {
console.log("Galloping...");
super.move(distanceInMeters);
}
}

let sam = new Snake("Sammy the Python");
let tom: Animal = new Horse("Tommy the Palomino");

sam.move();
tom.move(34);

//Slithering...
//Sammy the Python moved 5m.
//Galloping...
//Tommy the Palomino moved 34m.

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

默认为 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' 是私有的.

当比较带有 privateprotected成员的类型的时候,如果其中一个类型里包含一个 private成员,那么只有当另外一个类型中也存在这样一个 private成员, 并且它们都是来自同一处声明时,才认为这两个类型是兼容的

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

class Rhino extends Animal {
constructor() { super("Rhino"); }
}

class Employee {
private name: string;
constructor(theName: string) { this.name = theName; }
}

let animal = new Animal("Goat");
let rhino = new Rhino();
let employee = new Employee("Bob");

animal = rhino;
animal = employee; // 错误: Animal 与 Employee 不兼容.

这个例子中RhinoAnimal类的子类, Employee类,其类型看上去与 Animal是相同的。
因为 AnimalRhino共享了来自 Animal里的私有成员定义 private name: string,因此它们是兼容的。
然而 当把 Employee赋值给 Animal的时候,得到一个错误,说它们的类型不兼容。 尽管 Employee里也有一个私有成员 name,但它明显不是 Animal里面定义的那个。

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()); //派生类的实例 可以通过公有方法获取 父类protected属性
console.log(howard.name); // 错误 派生类的实例 无法直接通过属性获取 父类protected属性

不能在 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修饰符

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

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 是只读的

参数属性

1
2
3
4
5
class Octopus {
readonly numberOfLegs: number = 8;
constructor(readonly name: string) {
}
}

存取器 (跟es6一样)

通过getters/setters来截取对对象成员的访问

只带有 get不带有 set的存取器自动被推断为 readonly

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

静态属性

每个实例想要访问这个属性的时候,都要在 origin前面加上类名。 如同在实例属性上使用 this.前缀来访问属性一样,这里我们使用 Grid.来访问静态属性

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
abstract class Animal {
abstract makeSound(): void; //先定义
move(): void {
console.log('roaming the earch...');
}
}

先定义 然后再子类中实现

抽象类中的抽象方法不包含具体实现并且必须在派生类中实现。
抽象方法的语法与接口方法相似。 两者都是定义方法签名但不包含方法体

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(); // 错误: 方法在声明的抽象类中不存在

函数

函数类型

为函数定义类型

给每个参数添加类型之后再为函数本身添加返回值类型

函数类型包含两部分:参数类型和返回值类型。 当写出完整函数类型的时候,这两部分都是需要的

只要参数类型是匹配的,那么就认为它是有效的函数类型,而不在乎参数名是否正确

返回值类型是函数类型的必要部分,如果函数没有返回任何值,也必须指定返回值类型为 void不能不写

1
2
3
4
5
6
7
8
9
10
11
12
13
function add(x: number, y: number): number {
return x + y;
}

let myAdd = function(x: number, y: number): number { return x + y; };

//函数实现
let myAdd: (x: number, y: number) => number =
function(x: number, y: number): number { return x + y; };

//只要参数类型是匹配的,那么就认为它是有效的函数类型,而不在乎参数名是否正确
let myAdd: (baseValue: number, increment: number) => number =
function(x: number, y: number): number { return x + y; };

推断类型

如果你在赋值语句的一边指定了类型但是另一边没有类型的话,TypeScript编译器会自动识别出类型:

1
2
3
4
5
6
// myAdd has the full function type
let myAdd = function(x: number, y: number): number { return x + y; };

// The parameters `x` and `y` have the type number
let myAdd: (baseValue: number, increment: number) => number =
function(x, y) { return x + y; };

可选参数和默认参数

传递给一个函数的参数个数必须与函数期望的参数个数一致

1
2
3
4
5
6
7
function buildName(firstName: string, lastName: string) {
return firstName + " " + lastName;
}

let result1 = buildName("Bob"); // error, too few parameters
let result2 = buildName("Bob", "Adams", "Sr."); // error, too many parameters
let result3 = buildName("Bob", "Adams"); // ah, just right

?实现可选参数的功能

JavaScript里,每个参数都是可选的,可传可不传。 没传参的时候,它的值就是undefined。 在TypeScript里我们可以在参数名旁使用 ?实现可选参数的功能

可选参数必须跟在必须参数后面

1
2
3
4
5
6
7
8
9
10
function buildName(firstName: string, lastName?: string) {
if (lastName)
return firstName + " " + lastName;
else
return firstName;
}

let result1 = buildName("Bob"); // works correctly now
let result2 = buildName("Bob", "Adams", "Sr."); // error, too many parameters
let result3 = buildName("Bob", "Adams"); // ah, just right

为参数提供一个默认值

1
2
3
4
5
6
7
8
function buildName(firstName: string, lastName = "Smith") {
return firstName + " " + lastName;
}

let result1 = buildName("Bob"); // works correctly now, returns "Bob Smith"
let result2 = buildName("Bob", undefined); // still works, also returns "Bob Smith"
let result3 = buildName("Bob", "Adams", "Sr."); // error, too many parameters
let result4 = buildName("Bob", "Adams"); // ah, just right

可选参数与末尾的默认参数共享参数类型

在所有必须参数后面的带默认初始化的参数都是可选的,与可选参数一样,在调用函数的时候可以省略。 也就是说可选参数与末尾的默认参数共享参数类型

1
2
3
4
5
6
7
function buildName(firstName: string, lastName?: string) {
// ...
}

function buildName(firstName: string, lastName = "Smith") {
// ...
}

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

如果带默认值的参数出现在必须参数前面,用户必须明确的传入 undefined值来获得默认值

1
2
3
4
5
6
7
8
function buildName(firstName = "Will", lastName: string) {
return firstName + " " + lastName;
}

let result1 = buildName("Bob"); // error, too few parameters
let result2 = buildName("Bob", "Adams", "Sr."); // error, too many parameters
let result3 = buildName("Bob", "Adams"); // okay and returns "Bob Adams"
let result4 = buildName(undefined, "Adams"); // okay and returns "Will Adams"

剩余参数

剩余参数会被当做个数不限的可选参数。 可以一个都没有,同样也可以有任意个。

编译器创建参数数组,名字是在省略号(...)后面给定的名字,可以在函数体内使用这个数组。

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

let employeeName = buildName("Joseph", "Samuel", "Lucas", "MacKinzie");
1
2
3
4
5
function buildName(firstName: string, ...restOfName: string[]) {
return firstName + " " + restOfName.join(" ");
}

let buildNameFun: (fname: string, ...rest: string[]) => string = buildName;

this

在js非严格模式下this:

1、自执行函数中的this一般都是window
2、给元素的某个事件绑定方法,当事件触发执行对应事件的时候,方法中的this一般都是元素本身
3、还有一种方式能快速区分this:当方法执行的时候,看方法前边是否有 . . .前边是谁this就是谁,没有 .this一般就是window

在js严格模式下this:

如果执行主体不明确,this指向的是undefined(非严格模式下指向的是window)

最好是传递 this参数

提供一个显式的 this参数,this参数是个假的参数,它出现在参数列表的最前面

1
2
3
function f(this: void) {
// make sure `this` is unusable in this standalone function 确保`this`在此独立函数中不可用
}
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
interface Card {
suit: string;
card: number;
}
interface Deck {
suits: string[];
cards: number[];
createCardPicker(this: Deck): () => Card;
}
let deck: Deck = {
suits: ["hearts", "spades", "clubs", "diamonds"],
cards: Array(52),
// NOTE: The function now explicitly specifies that its callee must be of type Deck
createCardPicker: function(this: Deck) {
return () => {
let pickedCard = Math.floor(Math.random() * 52);
let pickedSuit = Math.floor(pickedCard / 13);

return {suit: this.suits[pickedSuit], card: pickedCard % 13};
}
}
}

let cardPicker = deck.createCardPicker();
let pickedCard = cardPicker();

alert("card: " + pickedCard.card + " of " + pickedCard.suit);

现在TypeScript知道createCardPicker期望在某个Deck对象上调用。 也就是说 thisDeck类型的,而非any,因此--noImplicitThis不会报错了

重载

JavaScript里函数根据传入不同的参数而返回不同类型的数据是很常见的

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
let suits = ["hearts", "spades", "clubs", "diamonds"];  //红桃、黑桃、梅花、方块

function pickCard(x): any {
// 检查在处理的是对象或者数组
// 如果是对象或者数组 我们从中选一张牌
if (typeof x == "object") {
let pickedCard = Math.floor(Math.random() * x.length);
return pickedCard;
}
// 如果不是 就发给用户想要的牌
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);

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
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 {
// 检查在处理的是对象或者数组
// 如果是对象或者数组 我们从中选一张牌
if (typeof x == "object") {
let pickedCard = Math.floor(Math.random() * x.length);
return pickedCard;
}
// 如果不是 就发给用户想要的牌
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);

重载的pickCard函数在调用的时候会自动进行正确的类型检查
编译器查找重载列表时,尝试使用第一个重载定义。 如果匹配的话就使用这个不匹配查找下一个,因此,在定义重载的时候,一定要把最精确的定义放在最前面

泛型

在调用函数时确定入参与出参的类型

达到返回值的类型与传入参数的类型相同,的目的

使用泛型来创建可重用的组件,一个组件可以支持多种类型的数据。 这样用户就可以以自己的数据类型来使用组件。

泛型之Hello World

1
2
3
4
5
6
7
8
//不用泛型
function identity(arg: number): number {
return arg;
}
//使用any类型来定义函数
function identity(arg: any): any {
return arg;
}

当我们需要传入的类型与返回的类型相同的时候。我们可以发现any的一个不足:上边的代码,当传入一个数字时,可以返回任何类型的值

需要一种方法使返回值的类型传入参数的类型相同的时候,我们使用泛型

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

泛型函数两种使用方法

第一种是,传入所有的参数,包含类型参数

1
let output = identity<string>("myString");  // type of output will be 'string'

第二种利用了类型推论 – 即编译器会根据传入的参数自动地帮助我们确定T的类型

1
let output = identity("myString");  // type of output will be 'string'

没必要使用尖括号(<>)来明确地传入类型;编译器可以查看myString的值,然后把T设置为它的类型。 类型推论帮助我们保持代码精简和高可读性。如果编译器不能够自动地推断出类型的话,只能像上面那样明确的传入T的类型

使用泛型变量

使用泛型创建像identity这样的泛型函数时,必须把这些参数当做是任意或所有类型
泛型函数的参数可能是任意类型,所以我们没办法使用特定类型才有方法

1
2
3
4
5
//如果我们想同时打印出arg的长度  会报错
function loggingIdentity<T>(arg: T): T {
console.log(arg.length); // Error: T doesn't have .length
return arg;
}

假设我们想操作T类型的数组而不直接是T。由于我们操作的是数组,所以.length属性是应该存在的。

1
2
3
4
5
6
7
8
9
10
11
//泛型函数loggingIdentity,接收类型参数T和参数arg,参数arg是类型是T的数组,并返回元素也是类型是T的数组
function loggingIdentity<T>(arg: T[]): T[] {
console.log(arg.length); // Array has a .length, so no more error
return arg;
}

//另一种实现
function loggingIdentity<T>(arg: Array<T>): Array<T> {
console.log(arg.length); // Array has a .length, so no more error
return arg;
}

泛型类型

泛型函数的类型与非泛型函数的类型没什么不同,只是有一个类型参数在最前面,像函数声明一样:

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
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 = identity;

把泛型参数当作整个接口的一个参数,能清楚的知道使用的具体是哪个泛型类型(比如: Dictionary<string>而不只是Dictionary)。 这样接口里的其它成员也能知道这个参数的类型了。

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
9
10
11
12
13
14
15
class GenericNumber<T> {
zeroValue: T;
add: (x: T, y: T) => T;
}
//规定number类型
let myGenericNumber = new GenericNumber<number>();
myGenericNumber.zeroValue = 0;
myGenericNumber.add = function(x, y) { return x + y; };

//规定字符串类型
let stringNumeric = new GenericNumber<string>();
stringNumeric.zeroValue = "";
stringNumeric.add = function(x, y) { return x + y; };

console.log(stringNumeric.add(stringNumeric.zeroValue, "test"));

泛型约束

定义一个接口来描述约束条件。 创建一个包含 .length属性的接口,使用这个接口和extends关键字来实现约束:

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

function loggingIdentity<T extends Lengthwise>(arg: T): T {
console.log(arg.length); // 现在我们知道它具有.length属性,因此不再有错误
return arg;
}
//现在这个泛型函数被定义了约束,因此它不再是适用于任意类型
loggingIdentity(3); // Error, number doesn't have a .length property
//需要传入符合约束类型的值,必须包含必须的属性:
loggingIdentity({length: 10, value: 3});

在泛型约束中使用类型参数

可以声明一个类型参数,且它被另一个类型参数所约束

1
2
3
4
5
6
7
8
9
//用属性名从对象里获取这个属性。 并且我们想要确保这个属性存在于对象 obj上,因此我们需要在这两个类型之间使用约束
function getProperty(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: Argument of type 'm' isn't assignable to 'a' | 'b' | 'c' | 'd'.

在泛型里使用类类型

在TypeScript使用泛型创建工厂函数时,需要引用构造函数的类类型

1
2
3
function create<T>(c: {new(): T; }): T {
return new c();
}

使用原型属性推断并约束构造函数与类实例的关系

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
class BeeKeeper {
hasMask: boolean;
}

class ZooKeeper {
nametag: string;
}

class Animal {
numLegs: number;
}

class Bee extends Animal {
keeper: BeeKeeper;
}

class Lion extends Animal {
keeper: ZooKeeper;
}

function createInstance<A extends Animal>(c: new () => A): A {
return new c();
}

createInstance(Lion).keeper.nametag; // typechecks!
createInstance(Bee).keeper.hasMask; // typechecks!