TypeScript是JavaScript类型的超集,它可以编译成纯JavaScript。

TypeScript拥有JavaScript还没有的特性,例如:类型,接口,模块,类型注释。使用这些特性和功能,我们可以更容易开发大规模的JavaScript应用。

始于JavaScript,归于JavaScript

安装TypeScript

1
npm install -g typescript

安装完成后,我们就拥有了 tec 命令。可以使用这个命令将 TypeScript文件 编译成 JavaScript文件

1
tsc helloworld.ts

在helloworld.ts同目录下生成文件名相同的js文件。在线编译工具

基础类型

TypeScript 可以为变量声明类型。声明了类型的变量,编译器可以确保它在赋值时不会产生类型错误。在大型复杂的JavaScript应用中可以帮助开发者避免犯错。

基础类型:布尔值,数值,字符串,数组,空值,Null,Undefined,任意值 ,元组,枚举,Never。

布尔值,数值,字符串,数组

1
2
3
4
5
6
7
8
9
10
11
12
13
14
let isFalse: boolean = false; // 布尔值
let isTrue: Boolean = new Boolean(1) //注意,使用构造函数 Boolean 创造的对象不是布尔值
let createdByBoolean: boolean = Boolean(1); // 直接调用 Boolean 也可以返回一个 boolean 类型

let decLiteral: number = 6; // 数值类型
let hexLiteral: number = 0xf00d; //十六进制
let binaryLiteral: number = 0b1010; // 二进制
let octalLiteral: number = 0o744; // 八进制
let notANumber: number = NaN;
let infinityNumber: number = Infinity;

let n: string = "fan"; // 字符串

let list: number[] = [1,2,3,4,5,6] //数组

空值 Void

空值(void) 表示没有任何类型,当一个函数没有返回值时,我们可以将其返回值设为 void

1
2
3
function warnUser (): void {
alert("This is Nothing!")
}

声明一个void类型的变量没什么用处,因为它只能被赋予 undefiendnull

1
let unusable: void = undefined;

Null 和 Undefiend

TypeScript中,undefiend 和 null 都有各自的类型 undefiend 和 null。

1
2
3
// Not much else we can assign to these variables!
let u: undefined = undefined;
let n: null = null;

默认情况下 null 和 undefined 是所有类型的子类型。你可以将 null 和 undefined 赋值给number/string类型的变量。
你可以通过在 tsconfig.json 指定 –strictNullChecks标记,null 和 undefined 只能赋值给void和它们各自。

任意值 Any

不确定变量会是什么类型时,可以将设为any。

1
2
3
let notSure: any = 4;
notSure = "maybe a string instead"
notSure = false;

可以将数组设置为 any类型,它就可以包含不同类型的数据。

1
2
let list: any[] = [1,true,"free"];
list[1] = 100;

元组 Tuple

元组:一个已知元素数量和类型的数组。

1
2
3
4
5
6
let x:[string, number]; // Declare a tuple type
x = ["hello", 10]; // Initialize it
x = [10,"hello"] //Error

console.log(x[0].substr(1)) // Ok
console.log(x[1].substr(1)) // Error 数值没有substr 方法

当访问一个越界的元素,它类型会被限制为元组中已有的类型

1
2
3
let x = [string, number];
x = ["Tuple",1,"TypeScript"]; // OK
x[3] = false // Error x 元组中并不存在 布尔值

枚举 Enum

枚举:为一组数值赋予一个名字。每个数值拥有一个下标(类似于数组下标),但你可以手动设置数值下标。

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

enum Color {Red = 1, Green, Blue};
let c: Color = Color.Red;

Never

Never: 永不存在的值类型。例如: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) {
}
}

类型断言

某个变量被设为 任意值,但某种情况下你能够确定其类型。可以使用类型断言告诉编译器这个变量当前类型。

1
2
3
4
5
6
7
// 尖括号 语法
let someValue: any = "this is a string";
let strLength: number = (<string>someValue).length;

// as 语法
let someValue: any = "this is a string";
let strLength: number = (someValue as string).length;

类型推论

TypeScript里,在没有明确类型的地方,类型推论会帮助提供类型。

变量推断类型

当变量未设定类型,TypeScript 根据它被赋值的类型,设置类型。

1
2
let x = 3; // x 并没有指定类型
x = 'Hello' // Error x是number类型不能被赋予string

数组推断类型

当一个数组中值为多个不同类型时,TypeScript会为它设定一个联合类型

1
2
let x = [0,1,null];
x.push(true); // Error 布尔值不能添加类型为number数组中。

当数组中联合类型并没有想要的类型,我们需要手动设定类型。

1
let zoo: Animal[]=[new Rhino(), new Elephant()];

上下文类型

TypeScript 会根据上下文推断类型。上下文归类会发生在表达式的类型与所处的位置相关时

1
2
3
4
5
6
7
window.onmousedown = function(mouseEvent) {
console.log(mouseEvent.button); //<- Error mouseEvent 被设定为mouseEvent类型
};

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

接口 Interface

TypeScript的重要功能就是对值进行类型检查,我们使用接口(Interfaces)来定义对象的类型。当转译成 JavaScript时,接口会消失-它们唯一的目的是在开发阶段起到辅助的作用。

例子

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
interface Person {
name: string;
age: number;
}
let customer: Person = {
name: 'jie hu',
age: 32
}

interface LabelledValue {
label: string;
}
function printLabel (labelledObj: LabelledValue) {
console.log(labelledObj.label)
}
let myObj = {size:10,label:"Size 10 Object"};
printLabel(myObj);

上面例子中,对象customer 属性名称,属性值类型和属性数量必须和接口Person 一致。
函数printLabel的对象参数labelledObj中一定要包含属性label。
__注意:__我们传入的对象参数实际上会包含很多属性,但是编译器只会检查那些必需的属性是否存在,并且其类型是否匹配。

可选属性

接口的属性可以定义为可选。当进行类型检查时,可选的属性没有定义也是可以通过。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
interface Person {
name: string;
age?: number;
}
let customer: Person = {
name: "jie hu"
};
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. 可以捕获引用不存在的属性错误。

只读属性

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

1
2
3
4
5
6
interface Point {
readonly x: number;
readonly y: number;
}
let p1:Point = {x: 10, y: 20}
p1.x = 5 ;//Error

额外的属性检查

1
2
3
4
5
6
7
8
9
interface SquareConfig {
color?: string;
number?: number;
}
function createSquare(config: SquareConfig) {
//
};
//Object literal may only specify known properties, and 'height' does not exist in type 'SquareConfig'
let mySquare = createSquare({color: "black",height:100});

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

1
2
//Object literal may only specify known properties, and 'height' does not exist in type 'SquareConfig'
let mySquare = createSquare({color: "black",height:100});

绕开检查有三种方式
使用 类型断言

1
let mySquare = createSquare({color: "black",height:100} as SquareConfig);

添加一个字符串索引签名

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

将对象赋值给变量。

1
2
let obj = {color:"block";height:100};
let mySquare = createSquare(obj)

函数类型

接口能够描述JavaScript中各种对象。除了普通对象外,接口还可以描述函数类型。
为了使用接口表示函数类型,我们需要定义一个调用签名。

1
2
3
4
5
6
7
8
9
10
11
12
interface SearchFunc {
(source: string, subString: string):boolean
}
let mySearch: SearchFunc;
mySearch = function (source: string, subString: string) {
let result = source.search(subString);
if (result == -1) {
return false;
} else {
return true;
}
}

调用签名 只要两个部分组成:(参数名称:参数类型):返回值类型

可索引的类型

接口可以描述通过索引的值的类型 比如数组

1
2
3
4
5
interface StringArray {
[index: number]: string;
}
let myArray: StringArray;
myArray = ["Bob","Fred"];

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

1
2
3
4
5
6
7
class Animal {name:string};
class Dog extends Animal {breed: string};
// Error: indexing with a 'string' will sometimes get you a Dog!
interface NotoKay {
[x: number]: Animal;
[x: string]: Dog
}

已确定的属性类型必须是字符串索引类型的子类型

1
2
3
4
5
interface NumberDictionary {
[index: string]: number;
length:number;
name: string; // Error `name`的类型不是索引类型的子类型
}

class类型

接口也可以去定义class的类型。强制class去符合某种契约。

1
2
3
4
5
6
7
8
9
10
11
interface ClockInterface {
currentTime: Date;
setTime(d: Date);
}
class Clock implements ClockInterface {
currentTime: Date;
constructore (h: number, m: number){}
setTime (d: Date) {
this.currentTime = d;
}
}

class具有两个类型:静态部分的类型和实例部分的类型。
不要用构造器签名去定义一个接口并试图定义一个类去实现这个接口。

扩展接口

接口可以互相扩展,一个接口可以继承多个接口。

1
2
3
4
5
6
7
8
9
10
11
12
13
interface Shape {
color: string;
}
interface PenStroke {
penWidth: number;
}
interface Square extends Shape, PenStroke {
sideLength: number;
}
let square = <Square>{};
square.color = "block";
square.sideLength = 10;
square.penWidth = 5.0;

混合类型

一个接口同时可以描述多种类型

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

接口继承类

接口可以继承一个class类型,它会继承class的成员但不包含实现。
接口同样会继承到类的private和protected成员。 这意味着当你创建了一个接口继承了一个拥有私有或受保护的成员的类时,这个接口类型只能被这个类或其子类所实现(implement)。

类 Class

过去JavaScript通过构造函数实现类的概念,通过原型链实现继承。在ES6中终于迎来了Class。(实际上还是基于原型链)
TypeScript除了实现ES6中的功能外,还添加了其他特性。

class

1
2
3
4
5
6
7
8
9
10
class Greeter {
greeting: string;
constructor(message: string) {
this.greeting = message;
}
greet () {
return `Hello, ${this.greeting}`
}
}
let greeter = new Greeter("world");

上面例子中,我们声明一个 Greeter类。这个类有三个部分:greeting属性,一个构造函数,一个greet方法。

继承

1
2
3
4
5
6
7
8
9
10
11
12
13
class Animal {
name: string;
constructor(theName: string){this.name = theName};
eat (food: string) {
return `${this.name} 爱吃 ${food}!`
}
}
class Dog extends Animal {
constructor(name: string){super(name)};
eat(food: string) {
super.eat(food);
}
}

使用 extends 关键字定义基类。
包含构造函数的派生类必须调用super()执行基类的构造方法。

访问修饰符

public
如果你没有定义访问修饰符,默认为public。

1
2
3
4
5
6
7
class Animal {
public name: string;
public constructor(theName: string){this.name = theName}
public eat(food: string) {
console.log(`${this.name} 爱吃 ${food}`)
}
}

标记为 public 的成员在派生类或者实例中都可以访问到。
private
标记为 private 的成员,它就不能在声明它的类外部访问,即使是派生类。

1
2
3
4
5
class Animal {
private name: string;
constructor (theName: string){this.name = theName};
}
new Animal('Dog').name // Error

protected
标记为 protected 的成员,在派生类中可以访问,类的外部不能访问。
如果构造函数被标记成protected,这个类不能实例化,只能能够被继承。

1
2
3
4
5
6
7
8
9
10
11
12
13
class Animal {
protected name: string;
protected constructor(theName: string){this.name = theName};
}
class Dog extends Animal {
name: string;
constructor(theName: string) {
super(theName);
this.name = theName;
};
};
let d = new Dog('hu');
let cat = new Animal("mouse") // Error

readonly修饰符

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

1
2
3
4
5
6
7
8
9
10
	
class Animal {
readonly name: string;
constructor (theName: string) {
super(theName);
this.name = theName;
}
}
let dog = new Animal('hu');
dog.name = 'er' //Error

参数属性

上面的例子中,我们需要声明一个 name 成员,然后再构造函数通过传参给其赋值。
通过参数属性,在构造函数定义参数的访问修饰符。这样就可以自动定义并初始化成员。

1
2
3
class Animal {
constructor (public name: string){}
}

存取器 getters/setters

通过getters/setters对对象成员的访问,可以有效的控制成员的访问。

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

静态属性 Static

我们可以定义静态成员,这些属性属于class而不是实例。

1
2
3
4
5
6
7
8
class Animal {
static type = 'Animal';
static isAnimal (a) {
return a instanceof Animal
}
}
console.log(Animal.type); // Animal
console.log(Animal.isAnimal(new Animal())) // true

类的静态属性只能通过类名来调用。

抽象类 Abstract

通过 abstract 定义抽象类和抽象方法。
抽象类不能被实例化,只能被其他类继承。抽象类的抽象方法需被派生类实现。

1
2
3
4
5
6
7
abstract class Animal {
public name;
abstract eat (food: string);
}
class Dog extends Animal {
eat (food: string) {}
}

抽象方法只能在抽象类中定义。

函数

为函数定义类型

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

函数类型包含两部分:参数类型和返回值类型;

可选参数

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

1
2
3
4
5
6
function buildName(firstName: string, lastName: string) {
return firstName+" "+lastName;
}
let result1 = buildName("huang"); //Error
let result2 = buildName("huang","mu","qing") //Error
let result3 = buildName("huang","mu qing")//ok

可以通过在参数名后面加 ? 实现可选参数的功能。

1
2
3
4
5
6
function buildName(firstName: string, lastName?: string) {
return firstName+" "+lastName;
}
let result1 = buildName("huang"); //ok
let result2 = buildName("huang","mu","qing") //Error
let result3 = buildName("huang","mu qing")//ok

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

默认参数

TypeScript中可以给参数指定默认值,如果实参未传递,形参就使用默认值。

1
2
3
4
5
6
7
function buildName (firstName:string = 'huang',lastName: string) {
return firstName + " " + lastName
}
let result1 = buildName('mu qing'); // Error
let result2 = buildName("huang","mu","qing"); // Error
let result3 = buildName("huang","mu qing"); //ok
let result4 = buildName(undefined, "mu qing") //ok

剩余参数

可以使用 …rest 方式获取剩余参数

1
2
3
4
function buildName(firstName: string, ...restOfName: string[]) {
return firstName + " " + restOfName.join(" ");
}
let result1 = buildName("huang","mu","qing");

重载

同一个函数,参数的类型不同,返回的值类型也会不一样。

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

泛型 Generics

泛型(Generics)允许同一个函数接受不同类型参数的一种模板。相比于使用any类型,使用泛型来创建可复用的组件更好,因为泛型会保留参数的类型。

泛型之Hello World

定义一个函数 identity,输入参数类型等于返回参数类型。

1
function identity(arg: any): any {return arg};

使用 any 类型后,这个函数能就收任意类型的参数,但返回的类型也是任意的,并不会是参入参数的类型。
我们需要确保返回值的类型和传入参数的类型相同。我们可以使用 __类型变量__,它是一种特殊的变量,只用于表示类型而不是值。

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

我们给函数添加了一个类型变量 T__。__T 捕获到了传入参数的类型。我们可以将__T__ 做为返回值的类型。现在我们确保返回值的类型等于传入参数的类型。
identity 函数叫做泛型。
泛型函数的调用,有两种方式。

1
2
let output1 = identity<string>("myString");//传入参数类型
let output2 = idenity("myString")// 类型推论

泛型约束

函数内部使用泛型时,并不知道它是哪种类型,拥有什么属性和方法,不能够随意操作它们。

1
2
3
4
function identity<T>(arg: T): T {
console.log(arg.length); // Error
return arg;
}

泛型 T 不一定包含属性 length;
我们可以对泛型进行约束,只允许拥有 length属性的参数传入。

1
2
3
4
5
6
7
interface Lengthwish {
length: number;
}
function identity<T extends Lengthwish>(arg: T): T {
console.log(arg.length);
return arg;
}

我们使用 extends 约束了泛型 T 必须符合接口 __Lengthwish__。

泛型接口

使用接口方式定义一个函数需要符合的 shape

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;
createArray = function<T>(length: number, value: T): Array<T> {
let result = [];
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
class GenericNumber<T> {
zeroValue: T;
add: (x: T, y: T) => T;
}

let myGenericNumber = new GenericNumber<number>();
myGenericNumber.zeroValue = 0;
myGenericNumber.add = function(x, y) { return x + y; };

项目配置

使用tsconfig.json

TypeScript编译器认为存在 tsconfig.json 文件的目录是TypeScript项目的根目录。
tsconfig.json文件中指定了用来编译项目的根文件和配置项。

不带任何输入文件的情况下调用 tsc,编译器会从当前目录开始去查找tsconfig.json文件,逐级向上搜索父目录。

不带任何输入文件的情况下调用tsc,且使用命令行参数–project(或-p)指定一个包含tsconfig.json文件的目录。

tsconfig.json文件常用有四个属性:
“compileerOptions” — 配置项
“files” — 相对或绝对文件路径
“include” — 指定被编译文件glob匹配模式列表
“exclude” — 指定不被编译文件glob匹配模式列表

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
{
"compilerOptions": {
"module": "system", --- 指定生成那种模块代码("CommonJS","AMD","System","ES6"
"noImplicitAny": true, ---在表达式和声明上有隐含的any类型时报错。
"removeComments": true, --- 删除所有注释,除了以/!*开头的版权
"outFile": "./built/tsc.js", ---将输出文件合并为一个文件。(只能在module是 System,amd情况下使用)
"outDir":"./build" --- 指定输出目录
"sourceMap": true --- 生成相应的.map文件。
},
"files": [
"a.ts",
"b.ts"
],
"include": [
"src/**/*"
],
"exclude": [
"node_modules",
"**/*.spec.ts"
]
}

使用Webpack构建

1
2
3
npm install ts-loader -D
npm install typescript -g
npm link typescript

webpack.config.js Demo

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
module.exports = {
entry:{
'd':'./d.ts',
'c':'./c.ts'
},
output:{
filename :"./build/[hash].[name].js"
},
module:{
rules: [
{
test: /\.tsx?$/,
loader: 'ts-loader'
}
]
},
resolve: {
extensions:['.ts','.tsx']
}
}

参考:

https://www.tslang.cn/docs/tutorial.html
https://github.com/xcatliu/typescript-tutorial/blob/master/README.md
https://zhongsp.gitbooks.io/typescript-handbook/content/
http://blog.hszofficial.site/js_Environment_Tools/TypeScript.html