介绍
TypeScript是一种由微软开发的自由和开源的编程语言,它是JavaScript的一个超集,扩展了JavaScript的语法。
TypeScript如它的名字一样,最广为人知的特点是增加了类型系统,是一门强类型语言。
安装必要的插件:
cnpm i vue-class-component vue-property-decorator --save
cnpm i ts-loader typescript tslint tslint-loader tslint-config-standard --save-dev
Tip:这些库功能大体如下:
vue-class-component:强化 Vue 组件,使用 TypeScript/装饰器 增强 Vue 组件
vue-property-decorator:在 vue-class-component上增强更多的结合 Vue 特性的装饰器
ts-loader:TypeScript 为 Webpack 提供了ts-loader,其实就是为了让webpack识别 .ts .tsx文件
tslint-loader跟tslint:.ts .tsx文件 约束代码格式(作用等同于eslint)
tslint-config-standard:tslint 配置 standard风格的约束
1. 基本类型
- 布尔值:
let isDone: boolean = false;
- 数字
let isNumber: number = 6;
- 字符串
let name: string = "anna";
name = "gril";
- 数组
let list: number[] = [1, 2, 3];
let list: Array<number> = [1, 2, 3];
- 元组
允许表示一个已知元素数量和类型的数组,各元素的类型不必相同。
let x: [string, number];
x = ['hello', 11];
- 枚举
enum Color {Red, Green, Blue}
let c: Color = Color.Green;
- Any
在编程阶段还不清楚类型的变量指定一个类型。Object类型的变量只是允许你给它赋任意值 - 但是却不能够在它上面调用任意的方法,即便它真的有这些方法
let notSure: any = 4;
notSure = "maybe a string instead";
notSure = false; // okay, definitely a boolean
- Void
void类型像是与any类型相反,它表示没有任何类型。 当一个函数没有返回值时,你通常会见到其返回值类型是 void:
function warnUser(): void {
console.log("This is void类型");
}
声明一个void类型的变量没有什么大用,因为你只能为它赋予undefined和null:
let unusable: void = undefined;
- Null 和 Undefined
undefined类型的变量只能赋值为undefined;
null类型的变量只能赋值为null
let u: undefined = undefined;
let n: null = null;
- Never
never类型表示的是那些永不存在的值的类型。
never类型是任何类型的子类型,也可以赋值给任何类型;然而,没有类型是never的子类型或可以赋值给never类型(除了never本身之外)。 即使 any也不可以赋值给never。
// 返回never的函数必须存在无法达到的终点
function error(message: string): never {
throw new Error(message);
}
// 推断的返回值类型为never
function fail() {
return error("Something failed");
}
- Object
object表示非原始类型,也就是除number,string,boolean,symbol,null或undefined之外的类型。
declare function create(o: object | null): void;
create({prop: 0}); //OK
create(null); //OK
create(11); //Error
- 组合类型(也叫联合类型)
let num: (number | string);
num = 123; // 正常
num = '456'; // 正常
num = true; // 报错
- 类型断言
类型断言好比其它语言里的类型转换,但是不进行特殊的数据检查和解构。
两种形式是等价的。 至于使用哪个大多数情况下是凭个人喜好;然而,当你在TypeScript里使用JSX时,只有 as语法断言是被允许的。
类型断言有两种形式。 其一是“尖括号”语法:
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;
2.变量声明
- var
作用域为该语句所在的函数内,且存在变量提升(var作用域或函数作用域)。 - let
作用域为该语句所在的代码块内,不存在变量提升(词法作用域或块作用域)。
不能在被声明之前读或写(暂时性死区)。 - const
作用域与let相同。
声明的是变量,在后面的代码中不允许再修改该变量的值。
只声明不赋值,会抛出语法错误。
注意:const 有let 的所有特性,const 不一定是常量。 - 解构
数组解构:
let [first, ...rest] = [1, 2, 3, 4];//var _b = [1, 2, 3, 4], first1 = _b[0], rest = _b.slice(1);
console.log(first); // outputs 1
console.log(rest); // outputs [ 2, 3, 4 ]
对象解构:
let o = {
aa: "object",
bb: 12,
cc: true
};
let { aa, bb } = o;//var aa = o.aa, bb = o.bb;
可以用没有声明的赋值:
需要用括号将它括起来,因为Javascript通常会将以 { 起始的语句解析为一个块。
({ aa, bb } = { aa: "abc", bb: 101 });
//(_a = { aa: "abc", bb: 101 }, aa = _a.aa, bb = _a.bb);
默认值:
默认值可以让你在属性为 undefined 时使用缺省值:
function keepWholeObject(wholeObject: { a: string, b?: number }) {
let { a, b = 1001 } = wholeObject;
}
//function keepWholeObject(wholeObject) {
var a = wholeObject.a, _a = wholeObject.b, b = _a === void 0 ? 1001 : _a;
}
- 函数声明
声明形参类型的语法与变量一样,都是 变量名:类型
声明返回值类型的语法需要写在小括号的后面 ():类型
function f({ a="", b=0 } = {}): void {
// ...
}
f();
//function f(_a) {
var _b = _a === void 0 ? {} : _a, _c = _b.a, a = _c === void 0 ? "" : _c, _d = _b.b, b = _d === void 0 ? 0 : _d;
// ...
}
f();
- 展开
展开操作符正与解构相反。 它允许你将一个数组展开为另一个数组,或将一个对象展开为另一个对象。(浅拷贝)
let first = [1, 2];
let second = [3, 4];
let bothPlus = [0, ...first, ...second, 5];
//var bothPlus = [0].concat(firstt, secondd, [5]);
3. 接口
TypeScript的核心原则之一是对值所具有的结构进行类型检查。 它有时被称做“鸭式辨型法”或“结构性子类型化”。
在TypeScript里,接口的作用就是为这些类型命名和为你的代码或第三方代码定义契约。
- 可选属性
接口里的属性不全都是必需的。 有些是只在某些条件下存在,或者根本不存在。 可选属性在应用“option bags”模式时很常用,即给函数传入的参数对象中只有部分属性赋值了。
interface SquareConfig {
color?: string;
width?: number;
}
- 只读属性
一些对象属性只能在对象刚刚创建的时候修改其值。 你可以在属性名前用 readonly来指定只读属性:
interface Point {
readonly x: number;
readonly y: number;
}
// p1.x = 11; error
TypeScript具有ReadonlyArray类型,它与Array相似,只是把所有可变方法去掉了,因此可以确保数组创建后再也不能被修改:
let aArr: number[]=[1,2,3,4];
let rArr: ReadonlyArray<number>=a;
//rArr[0]=11; error
//aAA=rArr; error
aArr = rArr; //可以用类型断言重写:
readonly vs const
做为变量使用的话用 const,若做为属性则使用readonly。
- 额外的属性检查
绕开这些检查非常简单。 最简便的方法是使用类型断言:
let mySquare = createSquare({ width: 100, opacity: 0.5 } as SquareConfig);
最佳的方式是能够添加一个字符串索引签名,前提是你能够确定这个对象可能具有某些做为特殊用途使用的额外属性。
interface SquareConfig {
color?: string;
width?: number;
[propName: string]: any;
}
还可以,将这个对象赋值给一个另一个变量
let squareOptions = { colour: "red", width: 100 };
let mySquare = createSquare(squareOptions);
- 函数类型
interface SearchFunc {
(source: string, subString: string): boolean;
}
- 可索引的类型
TypeScript支持两种索引签名:字符串和数字。 可以同时使用两种类型的索引,但是数字索引的返回值必须是字符串索引返回值类型的子类型。
interface StringArray {
[index: number]: string;
}
let myArray: StringArray;
myArray = ["Bob", "Fred"];
let myStr: string = myArray[0];
- 类类型
- 实现接口
TypeScript也能够用它来明确的强制一个类去符合某种契约。
interface ClockInterface {
currentTime: Date;
setTime(d: Date);//描述一个方法
}
class Clock implements ClockInterface {
currentTime: Date;
setTime(d: Date) {//在类里实现
this.currentTime = d;
}
constructor(h: number, m: number) { }
}
- 类静态部分与实例部分的区别
看下面的例子,我们定义了两个接口, ClockConstructor为构造函数所用和ClockInterface为实例方法所用。 为了方便我们定义一个构造函数 createClock,它用传入的类型创建实例。
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");
}
}
let digital = createClock(DigitalClock, 12, 17);
- 继承接口
和类一样,接口也可以相互继承。一个接口可以继承多个接口,创建出多个接口的合成接口。
interface Shape {
color: string;
}
interface Square extends Shape {
sideLength: number;
}
let square= <Square>{};
square.color="purpul";
square.sideLength= 11;
- 混合类型
一个例子就是,一个对象可以同时做为函数和对象使用,并带有额外的属性。
interface Counter {
(start: number): string;
interval: number;
reset(): void;
}
- 接口继承类
当接口继承了一个类类型时,它会继承类的成员但不包括其实现。 就好像接口声明了所有类中存在的成员,但并没有提供具体实现一样。
接口同样会继承到类的private和protected成员。
这意味着当你创建了一个接口继承了一个拥有私有或受保护的成员的类时,这个接口类型只能被这个类或其子类所实现(implement)。
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() { }
}
class Location {
}
4. 类
class Greeter {
greeting: string; //greeting的属性
constructor(message: string) {//构造函数
this.greeting = message;
}
greet() {//greet方法
return "Hello " + this.greeting;
}
}
let world = new Greeter("world");//使用 new构造了 Greeter类的一个实例
- 继承
class Animal {
//2.
name: string;
constructor(thisName: string){
this.name= thisName;
}
//1. 2.
move(distanceInMeters: number = 0) {
console.log(`Animal moved ${distanceInMeters}m.`);
}
}
//1. 最基本的继承:类从基类中继承了属性和方法。 Dog是一个 派生类,它派生自 Animal 基类,通过 extends关键字。 派生类通常被称作 子类,基类通常被称作 超类。
class Dog extends Animal {
brak() {
console.log("Woof Woof");
}
}
const dog = new Dog();
dog.brak();//Woof Woof
dog.move(10);//Animal moved 10m.
//2. 派生类包含了一个构造函数,它 必须调用 super(),它会执行基类的构造函数。 而且,在构造函数里访问 this的属性之前,我们 一定要调用 super()。
class Cat extends Animal {
constructor(name:string){
super(name);
}
move(distanceInMeters= 12){
console.log("Miao Miao");
super.move(distanceInMeters);
}
}
let tom: Animal= new Cat("Tom");
tom.move(20);//在子类里可以重写父类的方法
- 公共,私有与受保护的修饰符
- 默认为 public
- 理解 private
当成员被标记成 private时,它就不能在声明它的类的外部访问。
class Animal {
private name: string;
constructor(theName: string) { this.name = theName; }
}
new Animal("Cat").name; // 错误: 'name' 是私有的.
- 理解 protected
protected修饰符与 private修饰符的行为很相似,但有一点不同, protected成员在派生类中仍然可以访问。
class Animal {
protected name: string;
protected constructor(thisName: string){
this.name= thisName;
}
// Dog 能够继承Animal
class Dog extends Animal {
//...
}
let dogName = new Dog("Tom", "Nini");
let john = new Animal("John"); // 错误: 'Animal' 的构造函数是被保护的
//构造函数也可以被标记成 protected。 这意味着这个类不能在包含它的类外被实例化,但是能被继承。
- readonly 修饰符
你可以使用 readonly关键字将属性设置为只读的。 只读属性必须在声明时或构造函数里被初始化。
参数属性:
class Octopus {
readonly numberOfLegs: number = 8;
constructor(readonly name: string) {//仅在构造函数里使用 readonly name: string参数来创建和初始化 name成员。 我们把声明和赋值合并至一处。
}
}
- 存取器
TypeScript支持通过getters/setters来截取对对象成员的访问。 它能帮助你有效的控制对对象成员的访问。
- 抽象类
抽象类做为其它派生类的基类使用。 它们一般不会直接被实例化。 不同于接口,抽象类可以包含成员的实现细节。 abstract关键字是用于定义抽象类和在抽象类内部定义抽象方法。抽象方法必须包含 abstract关键字并且可以包含访问修饰符。
abstract class Animal {
abstract makeSound(): void;
move(): void {
console.log("moving ...");
}
}
- 高级技巧
构造函数
let greeter: Greeter;//Greeter类的实例的类型是 Greeter
greeter = new Greeter("world");
把类当做接口使用
类定义会创建两个东西:类的实例类型和一个构造函数。 因为类可以创建出类型,所以你能够在允许使用接口的地方使用类。
5. 函数
和JavaScript一样,TypeScript函数可以创建有名字的函数和匿名函数。
- 函数类型
function add(x: number,y: number):number {
return x+y;
}
let myAdd=function(x: number,y: number):number{
return x+y;
}
- 可选参数和默认参数
TypeScript里的每个函数参数都是必须的。 这不是指不能传递
null或undefined作为参数,而是说编译器检查用户是否为每个参数都传入了值。
function buildName(firstName: string, lastName: string) {
return firstName + " " + lastName;
}
//在TypeScript里我们可以在参数名旁使用 ?实现可选参数的功能。
function buildName(firstName: string, lastName?: string) {
//...
}
//可以为参数提供一个默认值当用户没有传递这个参数或传递的值是undefined时
function buildName(firstName: string, lastName = "Smith") {
//...
}
可选参数必须跟在必须参数后面。
与普通可选参数不同的是,带默认值的参数不需要放在必须参数的后面。 如果带默认值的参数出现在必须参数前面,用户必须明确的传入 undefined值来获得默认值。
- 剩余参数
剩余参数会被当做个数不限的可选参数。
function buildName(firstName: string, ...restOfName: string[]) {
return firstName + " " + restOfName.join(" ");
}
- this
- this和箭头函数
顶级的非方法式调用会将 this视为window。 (注意:在严格模式下, this为undefined而不是window)。
箭头函数能保存函数创建时的 this值,而不是调用时的值:
return () => {
//...
}
- this 参数
function f(this: void) {
// make sure `this` is unusable in this standalone function
}
- this参数在回调函数里
interface UIElement {
addClickListener(onclick: (this: void, e: Event) => void): void;
}
- 重载
为同一个函数提供多个函数类型定义来进行函数重载。
6. 泛型
在像C#和Java这样的语言中,可以使用泛型来创建可重用的组件,一个组件可以支持多种类型的数据。
- 泛型之Hello World
function identity<T>(arg: T): T {
return arg;
}
//identity函数叫做泛型,因为它可以适用于多个类型。
//两种方法使用。 第一种是,传入所有的参数,包含类型参数:
let output=identity<string>("myString");
//第二种方法更普遍。利用了类型推论 -- 即编译器会根据传入的参数自动地帮助我们确定T的类型:
let output=identity("myString");
- 使用泛型变量
function loggingIdentity<T>(arg: Array<T>): Array<T> {
console.log(arg.length);
return arg;
}
//泛型函数loggingIdentity,接收类型参数T和参数arg,它是个元素类型是T的数组,并返回元素类型是T的数组。
- 泛型类型
有一个类型参数在最前面,像函数声明一样:
也可以使用不同的泛型参数名,只要在数量上和使用方式上能对应上就可以。
let myIdentity: <U>(arg: U)=> U = identity;
还可以使用带有调用签名的对象字面量来定义泛型函数:
let myIdentity2: {<T>(arg: T): T} = identity;
把上面例子里的对象字面量拿出来做为一个接口:
interface GeneraIdentityFun<T> {
//<T>(arg: T): T;
(arg: T): T;
}
let myIdentity3 : GeneraIdentityFun<number> = identity;
- 泛型类
泛型类看上去与泛型接口差不多。 泛型类使用( <>)括起泛型类型,跟在类名后面。
class GeneraNumber<T> {
zeroValue: T;
add: (x: T, y: T) => T;
}
let myGeneraString = new GeneraNumber<string>();
myGeneraString.zeroValue = "zero";
myGeneraString.add = function (x, y) {
return x + y;
}
console.log(myGeneraString.add(myGeneraString.zeroValue,"test"));
- 泛型约束
interface LengthWise {
length: number;
}
function longIdentity<T extends LengthWise>(arg: T): T {
console.log(arg.length);
return arg;
}
//longIdentity(3);
longIdentity({length: 10, value: 3});
- 在泛型约束中使用类型参数
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使用泛型创建工厂函数时,需要引用构造函数的类类型。比如,
function createFun<T>(c: {new(): T;}): T {
return new c();
}
7. 枚举
使用枚举我们可以定义一些带名字的常量。
- 数字枚举
enum Diretion {
Up=1,
Down,
Left,
Right
}
//定义了一个数字枚举, Up使用初始化为 1。 其余的成员会从 1开始自动增长。
//默认Up的值为 0, Down的值为 1等等。
//通过枚举的属性来访问枚举成员,和枚举的名字来访问枚举类型:
console.log(Diretion.Down); //2
//简短地说,不带初始化器的枚举或者被放在第一的位置,或者被放在使用了数字常量或其它常量初始化了的枚举后面。
enum E {
A = getSomeValue(),
B, // error! 'A' is not constant-initialized, so 'B' needs an initializer
}
- 字符串枚举
在一个字符串枚举里,每个成员都必须用字符串字面量,或另外一个字符串枚举成员进行初始化。
enum Diretion {
A="up",
B="down"
}
- 异构枚举
从技术的角度来说,枚举可以混合字符串和数字成员,但是似乎你并不会这么做:
enum BooleanLikeHeterogeneousEnum {
No = 0,
Yes = "YES",
}
- 计算的和常量成员
每个枚举成员都带有一个值,它可以是 常量或 计算出来的。
enum E1 { X, Y, Z }
enum E2 {
A = 1, B, C
}
枚举成员使用 常量枚举表达式初始化。 常数枚举表达式是TypeScript表达式的子集,它可以在编译阶段求值。 当一个表达式满足下面条件之一时,它就是一个常量枚举表达式:
一个枚举表达式字面量(主要是字符串字面量或数字字面量)
一个对之前定义的常量枚举成员的引用(可以是在不同的枚举类型中定义的)
带括号的常量枚举表达式
一元运算符 +, -, ~其中之一应用在了常量枚举表达式
常量枚举表达式做为二元运算符 +, -, *, /, %, <<, >>, >>>, &, |, ^的操作对象。 若常数枚举表达式求值后为 NaN或 Infinity,则会在编译阶段报错。
enum FileAccess {
None, //0
Read = 1 << 1, //2
Write = 1 << 2, //4
ReadWrite = Read | Write, //6
G = "123".length
}
- 联合枚举与枚举成员的类型
存在一种特殊的非计算的常量枚举成员的子集:字面量枚举成员。 字面量枚举成员是指不带有初始值的常量枚举成员,或者是值被初始化为:
任何字符串字面量(例如: “foo”, “bar”, “baz”)
任何数字字面量(例如: 1, 100)
应用了一元 -符号的数字字面量(例如: -1, -100)
//枚举成员成为了类型
enum ShapeKind {
Circle,
Square
}
interface Circle {
kind: ShapeKind.Circle;
radius: number
}
let cir: Circle = {
kind: ShapeKind.Circle,
radius: 100
}
//另一个变化是枚举类型本身变成了每个枚举成员的 联合。
enum E {
Foo,
Bar,
}
function f(x: E) {
if (x !== E.Foo || x !== E.Bar) {
// ~~~~~~~~~~~
// Error! Operator '!==' cannot be applied to types 'E.Foo' and 'E.Bar'.
}
}
- 运行时的枚举
function funing(obj: {x: number}) {
return obj.X;
}
// Works, since 'E' has a property named 'X' which is a number.
funing(E1);
- 反向映射
enum Enum {
A
}
let aOfA= Enum.A;// 0
let nameofA = Enum[aOfA]; // A
console.log(Enum);// { 0: "A", A: 0 }
- const 枚举
const enum EnumConst {
A= 1,
B= A*2
}
let Diretions=[EnumConst.A, EnumConst.B];
//var Diretions = [1 /* A */, 2 /* B */];
- 外部枚举
外部枚举用来描述已经存在的枚举类型的形状。
外部枚举和非外部枚举之间有一个重要的区别,在正常的枚举里,没有初始化方法的成员被当成常数成员。 对于非常数的外部枚举而言,没有初始化方法时被当做需要经过计算的。
declare enum EnumDeclare {
a= 1,
b= a,
c=3
}
8.类型推论
- 最佳通用类型
- 上下文类型
9.类型兼容性
TypeScript里的类型兼容性是基于结构子类型的。 结构类型是一种只使用其成员来描述类型的方式。 它正好与名义(nominal)类型形成对比。
TypeScript结构化类型系统的基本规则是,如果x要兼容y,那么y至少具有与x相同的属性。
interface Named {
name: string;
}
class Person {
name: string;
}
let p: Named;
p= new Person();
- 比较两个函数
//参数列表略有不同:
let x = (a: number) => 0;
let y = (b: number, s: string) => 0;
y = x; // OK
x = y; // Error
//返回值类型不同的函数:
let x = () => ({name: 'Alice'});
let y = () => ({name: 'Alice', location: 'Seattle'});
x = y; // OK
y = x; // Error, because x() lacks a location property
//类型系统强制源函数的返回值类型必须是目标函数返回值类型的子类型。
函数参数双向协变
当比较函数参数类型时,只有当源函数参数能够赋值给目标函数或者反过来时才能赋值成功。
可选参数及剩余参数
比较函数兼容性的时候,可选参数与必须参数是可互换的。
当一个函数有剩余参数时,它被当做无限个可选参数。
函数重载
对于有重载的函数,源函数的每个重载都要在目标函数上找到对应的函数签名。
- 枚举
枚举类型与数字类型兼容,并且数字类型与枚举类型兼容。不同枚举类型之间是不兼容的。
enum Status { Ready, Waiting };
enum Color { Red, Blue, Green };
let status = Status.Ready;
status = Color.Green; // Error
- 类
类与对象字面量和接口差不多,但有一点不同:类有静态部分和实例部分的类型。 比较两个类类型的对象时,只有实例的成员会被比较。 静态成员和构造函数不在比较的范围内。
- 范型
因为TypeScript是结构性的类型系统,类型参数只影响使用其做为类型一部分的结果类型。
interface Empty<T> {
}
let x: Empty<number>;
let y: Empty<string>;
x = y; // OK, because y matches structure of x
nterface NotEmpty<T> {
data: T;
}
let x: NotEmpty<number>;
let y: NotEmpty<string>;
x = y; // Error, because x and y are not compatible
10.高级类型
- 交叉类型
交叉类型是将多个类型合并为一个类型。例如, Person & Serializable & Loggable同时是 Person 和 Serializable 和 Loggable。 就是说这个类型的对象同时拥有了这三种类型的成员。
function extend<T,U>(first: T,second: U): T & U {
let result= <T & U> {};
return result;
}
- 联合类型
联合类型与交叉类型很有关联,但是使用上却完全不同。 偶尔你会遇到这种情况,一个代码库希望传入 number或 string类型的参数。
function padLeft(value: string,padding: string | number) {
if(typeof padding === "number"){
return Array(padding+1).join(" ")+value;
}
if(typeof padding === "string"){
return padding+value;
}
throw new Error(`Expected string or number, got '${padding}'.`);
}
padLeft("Hello World", 4);
如果一个值是联合类型,我们只能访问此联合类型的所有类型里共有的成员。
interface Bird {
fly();
layEggs();
}
interface Fish {
swim();
layEggs();
}
function getSmallPet(): Fish | Bird {
// ...
}
let pet = getSmallPet();
pet.layEggs(); // okay
pet.swim(); // errors
- 类型保护与区分类型
- 用户自定义的类型保护
类型保护就是一些表达式,它们会在运行时检查以确保在某个作用域里的类型。 要定义一个类型保护,我们只要简单地定义一个函数,它的返回值是一个
类型谓词:
function isFish(pet: Fish | Bird): pet is Fish {
return (<Fish>pet).swim !== undefined;
}
//pet is Fish就是类型谓词。 谓词为 parameterName is Type这种形式, parameterName必须是来自于当前函数签名里的一个参数名。
- typeof类型保护
typeof类型保护只有两种形式能被识别: typeof v === "typename"和 typeof v !== “typename”, "typename"必须是 “number”, “string”, "boolean"或 “symbol”。
- instanceof类型保护
instanceof类型保护是通过构造函数来细化类型的一种方式。
instanceof的右侧要求是一个构造函数,TypeScript将细化为:
此构造函数的 prototype属性的类型,如果它的类型不为 any的话
构造签名所返回的类型的联合
if (padder instanceof StringPadder) {
padder; // 类型细化为'StringPadder'
}
- 可以为null的类型
–strictNullChecks标记可以解决此错误:当你声明一个变量时,它不会自动地包含 null或 undefined。 你可以使用联合类型明确的包含它们:
let s = "foo";
s = null; // 错误, 'null'不能赋值给'string'
let sn: string | null = "bar";
sn = null; // 可以
sn = undefined; // error, 'undefined'不能赋值给'string | null'
TypeScript会把 null和 undefined区别对待。 string | null, string | undefined和 string | undefined | null是不同的类型。
- 可选参数和可选属性
使用了 --strictNullChecks,可选参数会被自动地加上 | undefined:
unction f(x: number, y?: number) {
return x + (y || 0);
}
f(1, 2);
f(1);
f(1, undefined);
f(1, null); // error, 'null' is not assignable to 'number | undefined'
//可选属性也会有同样的处理:
class C {
a: number;
b?: number;
}
let c = new C();
c.a = undefined; // error, 'undefined' is not assignable to 'number'
c.b = undefined; // ok
c.b = null; // error, 'null' is not assignable to 'number | undefined'
- 类型保护和类型断言
由于可以为null的类型是通过联合类型实现,那么你需要使用类型保护来去除 null。
function fnull(sn: string | null): string {
return sn || "default";
}
//如果编译器不能够去除 null或 undefined,你可以使用类型断言手动去除。 语法是添加 !后缀: identifier!从 identifier的类型里去除了 null和 undefined
function fixed(name: string | null): string {
function postfix(epithet: string) {
return name!.charAt(0) + '. the ' + epithet; // ok
}
name = name || "Bob";
return postfix("great");
}
- 类型别名
类型别名有时和接口很像,但是可以作用于原始值,联合类型,元组以及其它任何你需要手写的类型。
type Name= string;
type NameFun= () => string;
type nameOrFun= Name | NameFun;
function getName (n: nameOrFun): Name {
if(typeof n === "string"){
return n;
}else{
return n();
}
}
//同接口一样,类型别名也可以是泛型 - 我们可以添加类型参数并且在别名声明的右侧传入:
type Container<T> = { value: T };
//也可以使用类型别名来在属性里引用自己:
type Tree<T> = {
value: T;
left: Tree<T>;
right: Tree<T>;
}
- 接口 vs. 类型别名
接口创建了一个新的名字,可以在其它任何地方使用。 类型别名并不创建新名字—比如,错误信息就不会使用别名。
在下面的示例代码里,在编译器中将鼠标悬停在 interfaced上,显示它返回的是 Interface,但悬停在
aliased上时,显示的却是对象字面量类型。
type Alias = { num: number }
interface Interface {
num: number;
}
declare function aliased(arg: Alias): Alias;
declare function interfaced(arg: Interface): Interface;
类型别名不能被 extends和 implements(自己也不能 extends和 implements其它类型)。
如果你无法通过接口来描述一个类型并且需要使用联合类型或元组类型,这时通常会使用类型别名。
- 字符串字面量类型
字符串字面量类型允许你指定字符串必须的固定值。
type Easing = "ease-in" | "ease-out" | "ease-in-out";
- 数字字面量类型
function rollDie(): 1 | 2 | 3 | 4 | 5 | 6 {
// ...
}
- 枚举成员类型
- 可辨识联合
可以合并单例类型,联合类型,类型保护和类型别名来创建一个叫做 可辨识联合的高级模式,它也称做 标签联合或 代数数据类型
每个接口都有 kind属性但有不同的字符串字面量类型。 kind属性称做 可辨识的特征或 标签。 其它的属性则特定于各个接口。
interface Square {
kind: "square";
size: number;
}
interface Rectangle {
kind: "rectangle";
width: number;
height: number;
}
type Shape = Square | Rectangle;
function area(s: Shape) {
switch (s.kind) {
case "square": return s.size * s.size;
case "rectangle": return s.height * s.width;
}
}
- 完整性检查
type Shape = Square | Rectangle | Triangle;
// should error here - we didn't handle case "triangle"
//首先是启用 --strictNullChecks并且指定一个返回值类型:
function area(s: Shape): number {
// error: returns number | undefined
// ...
}
//第二种方法使用 never类型,编译器用它来进行完整性检查:
function assertNever(x: never): never {
throw new Error("Unexpected object: " + x);
}
function area(s: Shape) {
//...
default: return assertNever(s); // error here if there are missing cases
}
- 多态的 this类型
多态的 this类型表示的是某个包含类或接口的 子类型。
- 索引类型(Index types)
function pluck(o, names) {
return names.map(n => o[n]);
}
function pluck<T, K extends keyof T>(o: T, names: K[]): T[K][] {
return names.map(n => o[n]);
}
interface Person {
name: string;
age: number;
}
//...
keyof T, 索引类型查询操作符
let personProps: keyof Person; // 'name' | 'age'
T[K], 索引访问操作符
function getProperty<T, K extends keyof T>(o: T, name: K): T[K] {
return o[name]; // o[name] is of type T[K]
}
//getProperty里的 o: T和 name: K,意味着 o[name]: T[K]。
索引类型和字符串索引签名
keyof和 T[K]与字符串索引签名进行交互。如果你有一个带有字符串索引签名的类型,那么 keyof T会是 string。 并且 T[string]为索引签名的类型:
interface Map<T> {
[key: string]: T;
}
let keys: keyof Map<number>; // string
let value: Map<number>['foo']; // number
- 映射类型
type Readonly<T> = {
readonly [P in keyof T]: T[P];
}
type Partial<T> = {
[P in keyof T]?: T[P];
}
// 使用
type PersonPartial = Partial<Person>;
type ReadonlyPerson = Readonly<Person>;
最简单的映射类型和它的组成部分:
type Keys = 'option1' | 'option2';
type Flags = { [K in Keys]: boolean };
它的语法与索引签名的语法类型,内部使用了 for … in。 具有三个部分:
类型变量 K,它会依次绑定到每个属性。
字符串字面量联合的 Keys,它包含了要迭代的属性名的集合。
属性的结果类型。
- 预定义的有条件类型
TypeScript 2.8在lib.d.ts里增加了一些预定义的有条件类型:
Exclude<T, U> – 从T中剔除可以赋值给U的类型。
Extract<T, U> – 提取T中可以赋值给U的类型。
NonNullable – 从T中剔除null和undefined。
ReturnType – 获取函数返回值类型。
InstanceType – 获取构造函数类型的实例类型。
11.Symbols
自ECMAScript 2015起,symbol成为了一种新的原生类型,就像number和string一样。
symbol类型的值是通过Symbol构造函数创建的。
let sym1 = Symbol();
let sym2 = Symbol("key");// 可选的字符串key
let sym3 = Symbol("key");
sym2 === sym3; // false, symbols是唯一的
// 像字符串一样,symbols也可以被用做对象属性的键。
let obj = {
[sym1]: "value"
};
console.log(obj[sym1]); // "value"
12.迭代器和生成器
- 可迭代性
当一个对象实现了Symbol.iterator属性时,我们认为它是可迭代的。 一些内置的类型如
Array,Map,Set,String,Int32Array,Uint32Array等都已经实现了各自的Symbol.iterator。
对象上的 Symbol.iterator函数负责返回供迭代的值。
for…of 语句
for…of会遍历可迭代的对象,调用对象上的Symbol.iterator方法。
let someArray = [1, "string", false];
for (let entry of someArray) {
console.log(entry); // 1, "string", false
}
for (let i in someArray) {
console.log(i); // "0", "1", "2",
}
for…of vs. for…in 语句
for…of和for…in均可迭代一个列表;但是用于迭代的值却不同,for…in迭代的是对象的 键
的列表,而for…of则迭代对象的键对应的值。
另一个区别是for…in可以操作任何对象;它提供了查看对象属性的一种方法。 但是 for…of关注于迭代对象的值。
let pets = new Set(["Cat", "Dog", "Hamster"]);
pets["species"] = "mammals";
for (let pet in pets) {
console.log(pet); // "species"
}
for (let pet of pets) {
console.log(pet); // "Cat", "Dog", "Hamster"
}
13.模块
“内部模块”现在称做“命名空间”。 “外部模块”现在则简称为“模块”,这是为了与 ECMAScript 2015里的术语保持一致,(也就是说 module X { 相当于现在推荐的写法 namespace X {)。
模块是自声明的;两个模块之间的关系是通过在文件级别上使用imports和exports建立的。
TypeScript与ECMAScript 2015一样,任何包含顶级import或者export的文件都被当成一个模块。相反地,如果一个文件不带有顶级的import或者export声明,那么它的内容被视为全局可见的(因此对模块也是可见的)。
- export 导出
- 导出声明
export interface StringValiator {
isAccept(s: string): boolean;
}
export const numberRegexp = /^[0-9]+$/;
export class ZipCode implements StringValiator {
isAccept(s: string) {
return s.length == 5 && numberRegexp.test(s);
}
}
- 导出语句
class ZipCode2 implements StringValiator {
isAccept(s: string) {
return s.length == 5;
}
}
export {ZipCode2};
- 重新导出
// 导出原先的验证器但做了重命名
export {ZipCodeValidator as RegExpBasedZipCodeValidator} from "./ZipCodeValidator";
// 或者一个模块可以包裹多个模块,并把他们导出的内容联合在一起通过语法:export * from "module"。
export * from "./StringValidator"; // exports interface StringValidator
- important 导入
// 导入一个模块中的某个导出内容
import { ZipCodeValidator } from "./ZipCodeValidator";
// 可以对导入内容重命名
import { ZipCodeValidator as ZCV } from "./ZipCodeValidator";
// 将整个模块导入到一个变量,并通过它来访问模块的导出部分
import * as validator from "./ZipCodeValidator";
// 具有副作用的导入模块。尽管不推荐这么做,一些模块会设置一些全局状态供其它模块使用。 这些模块可能没有任何的导出或用户根本就不关注它的导出。 使用下面的方法来导入这类模块:
import "./my-module.js";
- 默认导出
每个模块都可以有一个default导出。 默认导出使用 default关键字标记;并且一个模块只能够有一个default导出。
// ZipCodeValidator.ts
export default class ZipCodeValidator {
static numberRegexp = /^[0-9]+$/;
isAcceptable(s: string) {
return s.length === 5 && ZipCodeValidator.numberRegexp.test(s);
}
}
// Test.ts
import validator from "./ZipCodeValidator";
let myValidator = new validator();
export = 和 import = require()
CommonJS和AMD的exports都可以被赋值为一个对象, 这种情况下其作用就类似于 es6 语法里的默认导出,即 export default语法了。虽然作用相似,但是 export default 语法并不能兼容CommonJS和AMD的exports。
为了支持CommonJS和AMD的exports, TypeScript提供了export =语法。
export =语法定义一个模块的导出对象。 这里的对象一词指的是类,接口,命名空间,函数或枚举。
若使用export =导出一个模块,则必须使用TypeScript的特定语法import module = require(“module”)来导入此模块。
- 生成模块代码
import m= require("./module");
export let t=m.something + 1;
为了编译,我们必需要在命令行上指定一个模块目标。对于Node.js来说,使用–module commonjs; 对于Require.js来说,使用–module amd。比如:
tsc --module commonjs Test.ts
- 可选的模块加载和其它高级加载场景
import id = require("…")语句可以让我们访问模块导出的类型。 模块加载器会被动态调用(通过 require),就像下面if代码块里那样。 它利用了省略引用的优化,所以模块只在被需要时加载。
为了确保类型安全性,我们可以使用typeof关键字。 typeof关键字,当在表示类型的地方使用时,会得出一个类型值,这里就表示模块的类型。
- 使用其它的JavaScript库
要想描述非TypeScript编写的类库的类型,我们需要声明类库所暴露出的API。
我们叫它声明因为它不是“外部程序”的具体实现。 它们通常是在 .d.ts文件里定义的。
- 外部模块
使用与构造一个外部命名空间相似的方法,但是这里使用 module关键字并且把名字用引号括起来,方便之后import。
// node.d.ts (simplified excerpt)
declare module "url" {
// ...
}
// 可以/// <reference> node.d.ts并且使用
import url = require("url");或import * as URL from "url"加载模块。
- 外部模块简写
// declarations.d.ts
declare module "hot-new-module";
// 简写模块里所有导出的类型将是any。
import x, {y} from "hot-new-module";
x(y);
- 模块声明通配符
使用一个前缀或后缀来表示特殊的加载语法。 模块声明通配符可以用来表示这些情况。
declare module "*!text" {
const content: string;
export default content;
}
// Some do it the other way around.
declare module "json!*" {
const value: any;
export default value;
}
- UMD模块
// math-lib.d.ts
export function isPrime(x: number): boolean;
export as namespace mathLib;
// 之后,这个库可以在某个模块里通过导入来使用:
import { isPrime } from "math-lib";
isPrime(2);
mathLib.isPrime(2); // 错误: 不能在模块内使用全局定义。
- 创建模块结构指导
- 尽可能地在顶层导出
如果仅导出单个 class 或 function,使用 export default
// MyClass.ts
export default class SomeType {
constructor() { ... }
}
// MyFunc.ts
export default function getThing() { return 'thing'; }
如果要导出多个对象,把它们放在顶层里导出
export class SomeType { /* ... */ }
export function someFunc() { /* ... */ }
明确地列出导入的名字
import { ZipCode as NewZipCode, StringValiator } from './moduleTest';
使用命名空间导入模式当你要导出大量内容的时候
// MyLargeModule.ts
export class Dog { ... }
export class Cat { ... }
// Consumer.ts
import * as myLargeModule from "./MyLargeModule.ts";
let x = new myLargeModule.Dog();
- 使用重新导出进行扩展
class ProgrammerCalculator extends Calculator {
// ...
}
- 模块里不要使用命名空间
- 危险信号
文件的顶层声明是export namespace Foo { … } (删除Foo并把所有内容向上层移动一层)
文件只有一个export class或export function (考虑使用export default)
多个文件的顶层具有同样的export namespace Foo { (不要以为这些会合并到一个Foo中!)
14.命名空间
“内部模块”现在叫做“命名空间”。 任何使用 module关键字来声明一个内部模块的地方都应该使用namespace关键字来替换。
namespace Valiation {
// ...
}
- 分离到多文件
把Validation命名空间分割成多个文件。 尽管是不同的文件,它们仍是同一个命名空间,并且在使用的时候就如同它们在一个文件中定义的一样。
当涉及到多文件时,我们必须确保所有编译后的代码都被加载了。 我们有两种方式。
第一种方式,把所有的输入文件编译为一个输出文件,需要使用–outFile标记:
tsc --outFile sample.js Test.ts
第二种方式,我们可以编译每一个文件(默认方式),那么每个源文件都会对应生成一个JavaScript文件。 然后,在页面上通过
<script src="Test.js" type="text/javascript" />
- 别名
使用import q = x.y.z给常用的对象起一个短的名字。 不要与用来加载模块的 import x =require(‘name’)语法弄混了,这里的语法是为指定的符号创建一个别名。
namespace Shapes {
export namespace Polygons {
export class Triangle { }
export class Square { }
}
}
import polygons = Shapes.Polygons;
let sq = new polygons.Square(); // Same as "new Shapes.Polygons.Square()"
- 使用其它的JavaScript库
通常在 .d.ts里写这些声明。
// D3.d.ts (部分摘录)
declare namespace D3 {
export interface Selectors {
select: {
(selector: string): Selection;
(element: EventTarget): Selection;
};
}
export interface Event {
x: number;
y: number;
}
export interface Base extends Selectors {
event: Event;
}
}
declare var d3: D3.Base;
命名空间和模块
TypeScript里模块的一个特点是不同的模块永远也不会在相同的作用域内使用相同的名字。
- 对模块使用/// <reference
一个常见的错误是使用/// <reference 引用模块文件,应该使用import。
编译器首先尝试去查找相应路径下的.ts,.tsx再或者.d.ts。 如果这些文件都找不到,编译器会查找 外部模块声明。
- 不必要的命名空间
TypeScript里模块的一个特点是不同的模块永远也不会在相同的作用域内使用相同的名字。
// shapes.ts
export namespace Shapes {
export class Triangle { /* ... */ }
export class Square { /* ... */ }
}
// shapeConsumer.ts
import * as shapes from "./shapes";
let t = new shapes.Shapes.Triangle(); // shapes.Shapes?
下面是改进的例子:
// shapes.ts
export class Triangle { /* ... */ }
export class Square { /* ... */ }
// shapeConsumer.ts
import * as shapes from "./shapes";
let t = new shapes.Triangle();
不应该对模块使用命名空间,使用命名空间是为了提供逻辑分组和避免命名冲突。
- 模块的取舍
就像每个JS文件对应一个模块一样,TypeScript里模块文件与生成的JS文件也是一一对应的。
15. 模块解析
假设有一个导入语句 import { a } from “moduleA”; 为了去检查任何对 a的使用,编译器需要准确的知道它表示什么,并且需要检查它的定义moduleA。但 moduleA可能在你写的某个.ts/.tsx文件里或者在你的代码所依赖的.d.ts里。
- 相对 vs. 非相对模块导入
相对导入是以/,./或…/开头的。 下面是一些例子:
import Entry from "./components/Entry";
import { DefaultHeaders } from "../constants/http";
import "/mod";
所有其它形式的导入被当作非相对的。 下面是一些例子:
import * as $ from "jQuery";
import { Component } from "@angular/core";
相对导入在解析时是相对于导入它的文件,并且不能解析为一个外部模块声明。 你应该为你自己写的模块使用相对导入,这样能确保它们在运行时的相对位置。
非相对模块的导入可以相对于baseUrl或通过下文会讲到的路径映射来进行解析。 它们还可以被解析成 外部模块声明。 使用非相对路径来导入你的外部依赖。
- 模块解析策略
共有两种可用的模块解析策略:Node和Classic。 你可以使用 --moduleResolution标记来指定使用哪种模块解析策略。若未指定,那么在使用了 --module AMD | System | ES2015时的默认值为Classic,其它情况时则为Node。
Classic
这种策略在以前是TypeScript默认的解析策略。 现在,它存在的理由主要是为了向后兼容。
相对导入的模块是相对于导入它的文件进行解析的。
/root/src/folder/A.ts文件里的import { b } from “./moduleB"会使用下面的查找流程:
/root/src/folder/moduleB.ts
/root/src/folder/moduleB.d.ts
非相对模块的导入,编译器则会从包含导入文件的目录开始依次向上级目录遍历,尝试定位匹配的声明文件。
import { b } from “moduleB”,它是在/root/src/folder/A.ts文件里,会以如下的方式来定位"moduleB”:
/root/src/folder/moduleB.ts
/root/src/folder/moduleB.d.ts
/root/src/moduleB.ts
/root/src/moduleB.d.ts
/root/moduleB.ts
/root/moduleB.d.ts
/moduleB.ts
/moduleB.d.ts
Node
Node.js如何解析模块
这个解析策略试图在运行时模仿Node.js模块解析机制。
通常,在Node.js里导入是通过 require函数调用进行的。 Node.js会根据 require的是相对路径还是非相对路径做出不同的行为。
相对路径:
假设有一个文件路径为 /root/src/moduleA.js,包含了一个导入var x = require("./moduleB"); Node.js以下面的顺序解析这个导入:
- 检查/root/src/moduleB.js文件是否存在。
- 检查/root/src/moduleB目录是否包含一个package.json文件,且package.json文件指定了一个"main"模块。 在我们的例子里,如果Node.js发现文件 /root/src/moduleB/package.json包含了{ “main”: “lib/mainModule.js” },那么Node.js会引用/root/src/moduleB/lib/mainModule.js。
- 检查/root/src/moduleB目录是否包含一个index.js文件。 这个文件会被隐式地当作那个文件夹下的"main"模块。
非相对模块名:
Node会在一个特殊的文件夹 node_modules里查找你的模块。 node_modules可能与当前文件在同一级目录下,或者在上层目录里。 Node会向上级目录遍历,查找每个 node_modules直到它找到要加载的模块。
假设/root/src/moduleA.js里使用的是非相对路径导入var x = require(“moduleB”);。 Node则会以下面的顺序去解析 moduleB,直到有一个匹配上。
/root/src/node_modules/moduleB.js
/root/src/node_modules/moduleB/package.json (如果指定了"main"属性)
/root/src/node_modules/moduleB/index.js/root/node_modules/moduleB.js /root/node_modules/moduleB/package.json
(如果指定了"main"属性)
/root/node_modules/moduleB/index.js
/node_modules/moduleB.js /node_modules/moduleB/package.json
(如果指定了"main"属性)
/node_modules/moduleB/index.js
TypeScript如何解析模块
TypeScript是模仿Node.js运行时的解析策略来在编译阶段定位模块定义文件。 因此,TypeScript在Node解析逻辑基础上增加了TypeScript源文件的扩展名( .ts,.tsx和.d.ts)。 同时,TypeScript在 package.json里使用字段"types"来表示类似"main"的意义 - 编译器会使用它来找到要使用的"main"定义文件。
附加的模块解析标记
Base URL
在利用AMD模块加载器的应用里使用baseUrl是常见做法,它要求在运行时模块都被放到了一个文件夹里。 这些模块的源码可以在不同的目录下,但是构建脚本会将它们集中到一起。
设置baseUrl来告诉编译器到哪里去查找模块。 所有非相对模块导入都会被当做相对于 baseUrl。
baseUrl的值由以下两者之一决定:
命令行中baseUrl的值(如果给定的路径是相对的,那么将相对于当前路径进行计算)
‘tsconfig.json’里的baseUrl属性(如果给定的路径是相对的,那么将相对于‘tsconfig.json’路径进行计算)
路径映射
TypeScript编译器通过使用tsconfig.json文件里的"paths"来支持这样的声明映射。 下面是一个如何指定 jquery的"paths"的例子。
{
"compilerOptions": {
"baseUrl": ".", // This must be specified if "paths" is.
"paths": {
"jquery": ["node_modules/jquery/dist/jquery"] // 此处映射是相对于"baseUrl"
}
}
}
//请注意"paths"是相对于"baseUrl"进行解析。
{
"compilerOptions": {
"baseUrl": ".",
"paths": {
"*": [
"*",
"generated/*"
]
}
}
}
//它告诉编译器所有匹配"*"(所有的值)模式的模块导入会在以下两个位置查找:
"*": 表示名字不发生改变,所以映射为<moduleName> => <baseUrl>/<moduleName>
"generated/*"表示模块名添加了“generated”前缀,所以映射为<moduleName> => <baseUrl>/generated/<moduleName>
利用rootDirs指定虚拟目录
利用rootDirs,可以告诉编译器生成这个虚拟目录的roots; 因此编译器可以在“虚拟”目录下解析相对模块导入,就 好像它们被合并在了一起一样。
可以使用"rootDirs"来告诉编译器。 "rootDirs"指定了一个roots列表,列表里的内容会在运行时被合并。 因此,针对这个例子, tsconfig.json如下:
{
"compilerOptions": {
"rootDirs": [
"src/views",
"generated/templates/views"
]
}
}
跟踪模块解析
通过 --traceResolution启用编译器的模块解析跟踪,它会告诉我们在模块解析过程中发生了什么。
使用–noResolve
–noResolve编译选项告诉编译器不要添加任何不是在命令行上传入的文件到编译列表。 编译器仍然会尝试解析模块,但是只要没有指定这个文件,那么它就不会被包含在内。
16.声明合并
“声明合并”是指编译器将针对同一个名字的两个独立声明合并为单一声明。 合并后的声明同时拥有原先两个声明的特性。 任何数量的声明都可被合并;不局限于两个声明。
TypeScript中的声明会创建以下三种实体之一:命名空间,类型或值。
- 合并接口
最简单也最常见的声明合并类型是接口合并。 从根本上说,合并的机制是把双方的成员放到一个同名的接口里。
interface Box {
height: number;
width: number;
}
interface Box {
scale: string;
}
let box: Box= {height: 5, width: 5, scale: '10'};
接口的非函数的成员应该是唯一的。如果它们不是唯一的,那么它们必须是相同的类型。如果两个接口中同时声明了同名的非函数成员且它们的类型不同,则编译器会报错。
对于函数成员,每个同名函数声明都会被当成这个函数的一个重载。 同时需要注意,当接口 A与后来的接口 A合并时,后面的接口具有更高的优先级。
例外是当出现特殊的函数签名时。 如果签名里有一个参数的类型是 单一的字符串字面量(比如,不是字符串字面量的联合类型),那么它将会被提升到重载列表的最顶端。
- 合并命名空间
对于命名空间的合并,模块导出的同名接口进行合并,构成单一命名空间内含合并后的接口。
对于命名空间里值的合并,如果当前已经存在给定名字的命名空间,那么后来的命名空间的导出成员会被加到已经存在的那个模块里。 - 命名空间与类和函数和枚举类型合并
命名空间可以与其它类型的声明进行合并。 只要命名空间的定义符合将要合并类型的定义。合并结果包含两者的声明类型。 TypeScript使用这个功能去实现一些JavaScript里的设计模式。
// 合并命名空间和类
class Alubm {
label: Alubm.AlubmLabel;
}
namespace Alubm {
export class AlubmLabel {};// 必须导出 AlbumLabel类,好让合并的类能访问
}
// 扩展函数
function buildLabel(name: string): string {
return buildLabel.bb + name + buildLabel.aa;
}
namespace buildLabel {
export let aa = "";
export let bb = "Hello, ";
}
console.log(buildLabel("pjn"));// Hello, pjn
// 扩展枚举类
enum Color {
red = 1,
green = 2
}
namespace Color {
export function minColor(colorName: string) {
if(colorName == "red"){
return Color.red;
}else if(colorName == "white"){
return Color.red + Color.green;
}
}
}
- 非法的合并
TypeScript并非允许所有的合并。 目前,类不能与其它类或变量合并。
- 模块扩展
17.JSX
JSX是一种嵌入式的类似XML的语法。 它可以被转换成合法的JavaScript,尽管转换的语义是依据不同的实现而定的。
- 基本用法
使用JSX必须做两件事:
给文件一个.tsx扩展名
启用jsx选项
TypeScript具有三种JSX模式:preserve,react和react-native。
- as 操作符
类型断言操作符:as
var foo = bar as foo;
- 类型检查
固有元素总是以一个小写字母开头,基于值的元素总是以一个大写字母开头。
- 固有元素
固有元素使用特殊的接口JSX.IntrinsicElements来查找。
declare namespace JSX {
interface IntrinsicElements {
foo: any;
// [elementName: string]: any;
}
}
<foo />; // 正确
<bar />; // 错误
- 基于值的元素
基于值的元素会简单的在它所在的作用域里按标识符查找。
import MyComponent from "./myComponent";
<MyComponent />; // 正确
<SomeOtherComponent />; // 错误
有两种方式可以定义基于值的元素:
1.无状态函数组件 (SFC)
组件被定义成JavaScript函数,它的第一个参数是props对象。 TypeScript会强制它的返回值可以赋值给JSX.Element。还可以利用函数重载。
function ComponentFoo(prop: FooProp) {
return <AnotherComponent name={prop.name} />;
}
2.类组件
class MyComponent {
render() {}
}
// 使用构造签名
var myComponent = new MyComponent();
// 元素类的类型 => MyComponent
// 元素实例的类型 => { render: () => void }
function MyFactoryFunction() {
return {
render: () => {
}
}
}
// 使用调用签名
var myComponent = MyFactoryFunction();
// 元素类的类型 => FactoryFunction
// 元素实例的类型 => { render: () => void }
- 属性类型检查
属性类型检查的第一步是确定元素属性类型。
对于固有元素,这是JSX.IntrinsicElements属性的类型。
declare namespace JSX {
interface IntrinsicElements {
foo: { bar?: boolean }
}
}
// `foo`的元素属性类型为`{bar?: boolean}`
<foo bar />;
对于基于值的元素,它取决于先前确定的在元素实例类型上的某个属性的类型。 至于该使用哪个属性来确定类型取决于JSX.ElementAttributesProperty。
declare namespace JSX {
interface ElementAttributesProperty {
props; // 指定用来使用的属性名
}
}
class MyComponent {
// 在元素实例类型上指定属性
props: {
foo?: string;
}
}
// `MyComponent`的元素属性类型为`{foo?: string}`
<MyComponent foo="bar" />
- 子孙类型检查
children是元素属性(attribute)类型的一个特殊属性(property),子JSXExpression将会被插入到属性里。 与使用JSX.ElementAttributesProperty来决定props名类似,我们可以利用JSX.ElementChildrenAttribute来决定children名。
JSX.ElementChildrenAttribute应该被声明在单一的属性(property)里。
declare namespace JSX {
interface ElementChildrenAttribute {
children: {}; // specify children name to use
}
}
- JSX结果类型
默认地JSX表达式结果的类型为any。 你可以自定义这个类型,通过指定JSX.Element接口。 然而,不能够从接口里检索元素,属性或JSX的子元素的类型信息。 它是一个黑盒。
- 嵌入的表达式
JSX允许你使用{ }标签来内嵌表达式。
var a = (
<div>
{["foo", "bar"].map(function(i) {
return <span>{i / 2}</span>;
})}
</div>
);
- React整合
要想一起使用JSX和React,你应该使用React类型定义。 这些类型声明定义了JSX合适命名空间来使用React。
- 工厂函数
jsx: react编译选项使用的工厂函数是可以配置的。可以使用jsxFactory命令行选项,或内联的@jsx注释指令在每个文件上设置。
18.装饰器
装饰器(Decorators)为我们在类的声明及成员上通过元编程语法添加标注提供了一种方式。
若要启用实验性的装饰器特性,你必须在命令行或tsconfig.json里启用experimentalDecorators编译器选项:
命令行:
tsc --target ES5 --experimentalDecorators装饰器是一种特殊类型的声明,它能够被附加到类声明,方法, 访问符,属性或参数上。 装饰器使用 @expression这种形式,expression求值后必须为一个函数,它会在运行时被调用,被装饰的声明信息做为参数传入。
- 装饰器工厂
装饰器工厂就是一个简单的函数,它返回一个表达式,以供装饰器在运行时调用。
function color(value: string) {// 这是一个装饰器工厂
return function (targe) {// 这是装饰器
}
}
- 装饰器组合
多个装饰器可以同时应用到一个声明上,就像下面的示例:
书写在同一行上:
@f @g x
书写在多行上:
@f
@g
x
当多个装饰器应用于一个声明上,它们求值方式与复合函数相似。在这个模型下,当复合f和g时,复合的结果(f ∘ g)(x)等同于f(g(x))。
进行如下步骤的操作:
1.由上至下依次对装饰器表达式求值。
2.求值的结果会被当作函数,由下至上依次调用。
- 装饰器求值
类中不同声明上的装饰器将按以下规定的顺序应用: 1. 参数装饰器,然后依次是方法装饰器,访问符装饰器,或属性装饰器应用到每个实例成员。
2.参数装饰器,然后依次是方法装饰器,访问符装饰器,或属性装饰器应用到每个静态成员。
3.参数装饰器应用到构造函数。
4.类装饰器应用到类。
- 类装饰器
类装饰器在类声明之前被声明(紧靠着类声明)。 类装饰器应用于类构造函数,可以用来监视,修改或替换类定义。类装饰器不能用在声明文件中( .d.ts),也不能用在任何外部上下文中(比如declare的类)。
@sealed
class Greet {
greeting: string;
constructor(message: string){
this.greeting= message;
}
greet() {
return "Hello, "+ this.greeting;
}
}
function sealed(constructor: Function) {
Object.seal(constructor);
Object.seal(constructor.prototype);
}
- 方法装饰器
方法装饰器声明在一个方法的声明之前(紧靠着方法声明)。 它会被应用到方法的 属性描述符上,可以用来监视,修改或者替换方法定义。
方法装饰器不能用在声明文件( .d.ts),重载或者任何外部上下文(比如declare的类)中。
方法装饰器表达式会在运行时当作函数被调用,传入下列3个参数:
1.对于静态成员来说是类的构造函数,对于实例成员是类的原型对象。
2.成员的名字。
3.成员的属性描述符。
// 一个装饰器工厂,被调用时,它会修改属性描述符的enumerable属性。
@enumerable(false)
greet() {
return "Hello, " + this.greeting;
}
// 定义@enumerable装饰器:
function enumerable(value: boolean) {
return function (target: any, propertyKey: string, descriptor: PropertyDescriptor) {
descriptor.enumerable = value;
};
}
- 访问器装饰器
访问器装饰器声明在一个访问器的声明之前(紧靠着访问器声明)。 访问器装饰器应用于访问器的 属性描述符并且可以用来监视,修改或替换一个访问器的定义。 访问器装饰器不能用在声明文件中(.d.ts),或者任何外部上下文(比如 declare的类)里。
访问器装饰器表达式会在运行时当作函数被调用,传入下列3个参数:(如5)
@configurable(false)
get x() { return this._x; }
- 属性装饰器
属性装饰器声明在一个属性声明之前(紧靠着属性声明)。 属性装饰器不能用在声明文件中(.d.ts),或者任何外部上下文(比如 declare的类)里。
属性装饰器表达式会在运行时当作函数被调用,传入下列2个参数:
1.对于静态成员来说是类的构造函数,对于实例成员是类的原型对象。
2.成员的名字。
@format("Hello, %s")
greeting: string;
- 参数装饰器
参数装饰器声明在一个参数声明之前(紧靠着参数声明)。 参数装饰器应用于类构造函数或方法声明。
参数装饰器不能用在声明文件(.d.ts),重载或其它外部上下文(比如 declare的类)里。
参数装饰器表达式会在运行时当作函数被调用,传入下列3个参数:
1.对于静态成员来说是类的构造函数,对于实例成员是类的原型对象。
2.成员的名字。
3.参数在函数参数列表中的索引。
@validate
greet(@required name: string) {
return "Hello " + name + ", " + this.greeting;
}
// @required装饰器添加了元数据实体把参数标记为必需的。 @validate装饰器把greet方法包裹在一个函数里在调用原先的函数前验证函数参数。
// @required装饰器
import "reflect-metadata";
const requiredMetadataKey = Symbol("required");
function required(target: Object, propertyKey: string | symbol, parameterIndex: number) {
let existingRequiredParameters: number[] = Reflect.getOwnMetadata(requiredMetadataKey, target, propertyKey) || [];
existingRequiredParameters.push(parameterIndex);
Reflect.defineMetadata(requiredMetadataKey, existingRequiredParameters, target, propertyKey);
}
- 元数据
一些例子使用了reflect-metadata库来支持实验性的metadata API。
npm i reflect-metadata --save
TypeScript支持为带有装饰器的声明生成元数据。 你需要在命令行或 tsconfig.json里启用emitDecoratorMetadata编译器选项。
Command Line:
tsc --target ES5 --experimentalDecorators --emitDecoratorMetadata
TypeScript编译器可以通过@Reflect.metadata装饰器注入设计阶段的类型信息。
class Line {
private _p0: Point;
private _p1: Point;
@validate
@Reflect.metadata("design:type", Point)
set p0(value: Point) { this._p0 = value; }
get p0() { return this._p0; }
@validate
@Reflect.metadata("design:type", Point)
set p1(value: Point) { this._p1 = value; }
get p1() { return this._p1; }
}
19.Mixins
通过可重用组件创建类的方式,就是联合另一个简单类的代码。
混入示例:
https://www.tslang.cn/docs/handbook/mixins.html
// Disposable Mixin
class Disposable {
isDisposed: boolean;
dispose() {
this.isDisposed = true;
}
}
20.三斜线指令
三斜线指令是包含单个XML标签的单行注释。 注释的内容会做为编译器指令使用。
三斜线指令仅可放在包含它的文件的最顶端。 一个三斜线指令的前面只能出现单行或多行注释,这包括其它的三斜线指令。
/// <reference path="…" / 它用于声明文件间的 依赖。
三斜线引用告诉编译器在编译过程中要引入的额外的文件。
当使用–out或–outFile时,它也可以做为调整输出内容顺序的一种方法。 文件在输出文件内容中的位置与经过预处理后的输入顺序一致。
- 预处理输入文件
编译器会对输入文件进行预处理来解析所有三斜线引用指令。 在这个过程中,额外的文件会加到编译过程中。
一个三斜线引用路径是相对于包含它的文件的,如果不是根文件。
- 错误
引用不存在的文件会报错。 一个文件用三斜线指令引用自己会报错。
- 使用 --noResolve
如果指定了–noResolve编译选项,三斜线引用会被忽略;它们不会增加新文件,也不会改变给定文件的顺序。
/// <reference types="…" /
与 /// <reference path="…" / 指令相似,这个指令是用来声明 依赖的; 一个 /// <reference types="…" / 指令则声明了对某个包的依赖。
/// <reference no-default-lib=“true”/
这个指令把一个文件标记成默认库。这个指令告诉编译器在编译过程中不要包含这个默认库(比如,lib.d.ts)。 这与在命令行上使用 --noLib相似。
/// <amd-module /
默认情况下生成的AMD模块都是匿名的。 但是,当一些工具需要处理生成的模块时会产生问题,比如 r.js。///<amd-module name='NamedModule'/> export class C { }
/// <amd-dependency /
注意:这个指令被废弃了。使用import “moduleName”;语句代替。
/// 告诉编译器有一个非TypeScript模块依赖需要被注入,做为目标模块require调用的一部分。/// <amd-dependency path="legacy/moduleA" name="moduleA"/>
21.JavaScript文件类型检查
TypeScript 2.3以后的版本支持使用–checkJs对.js文件进行类型检查和错误提示。
你可以通过添加// @ts-nocheck注释来忽略类型检查;相反,你可以通过去掉–checkJs设置并添加一个// @ts-check注释来选则检查某些.js文件。 你还可以使用// @ts-ignore来忽略本行的错误。
- 用JSDoc类型表示类型信息
JSDoc注解修饰的声明会被设置为这个声明的类型。比如:
/** @type {number} */
var x;
x = 0; // OK
x = false; // Error: boolean is not assignable to number
- 属性的推断来自于类内的赋值语句
属性的类型是在构造函数里赋的值的类型,除非它没在构造函数里定义或者在构造函数里是undefined或null。 若是这种情况,类型将会是所有赋的值的类型的联合类型。
class C {
constructor() {
this.constructorOnly = 0
this.constructorUnknown = undefined
}
method() {
this.constructorOnly = false // error, constructorOnly is a number
this.constructorUnknown = "plunkbat" // ok, constructorUnknown is string | undefined
this.methodOnly = 'ok' // ok, but y could also be undefined
}
method2() {
this.methodOnly = true // also, ok, y's type is string | boolean | undefined
}
}
- 构造函数等同于类
ES2015以前,Javascript使用构造函数代替类。 编译器支持这种模式并能够将构造函数识别为ES2015的类。
function C() {
this.constructorOnly = 0
this.constructorUnknown = undefined
}
C.prototype.method = function() {
this.constructorOnly = false // error
this.constructorUnknown = "plunkbat" // OK, the type is string | undefined
}
- 支持CommonJS模块
在.js文件里,TypeScript能识别出CommonJS模块。 对exports和module.exports的赋值被识别为导出声明。 相似地,require函数调用被识别为模块导入。
- 类,函数和对象字面量是命名空间
.js文件里的类是命名空间。 它可以用于嵌套类,比如:
class C {
}
C.D = class {
}
ES2015之前的代码,它可以用来模拟静态方法:
function Outer() {
this.y = 2
}
Outer.Inner = function() {
this.yy = 2
}
它还可以用于创建简单的命名空间:
var ns = {}
ns.C = class {
}
ns.func = function() {
}
同时还支持其它的变化:
// 立即调用的函数表达式
var ns = (function (n) {
return n || {};
})();
ns.CONST = 1
// defaulting to global
var assign = assign || function() {
// code goes here
}
assign.extra = 1
- 对象字面量是开放的
对象字面量具有开放的类型,允许添加并访问原先没有定义的属性。例如:
var obj = { a: 1 };
obj.b = 2; // Allowed
// 与其它JS检查行为相似,这种行为可以通过指定JSDoc类型来改变,例如:
/** @type {{a: number}} */
var obj = { a: 1 };
obj.b = 2; // Error, type {a: number} does not have property b
- null,undefined,和空数组的类型是any或any[]
- 函数参数是默认可选的
function bar(a, b) {
console.log(a + " " + b);
}
bar(1); // OK, second argument considered optional
bar(1, 2);
bar(1, 2, 3); // Error, too many arguments
- 由arguments推断出的var-args参数声明
如果一个函数的函数体内有对arguments的引用,那么这个函数会隐式地被认为具有一个var-arg参数(比如:(…arg: any[]) => any))。使用JSDoc的var-arg语法来指定arguments的类型。
/** @param {...number} args */
function sum(/* numbers */) {
var total = 0
for (var i = 0; i < arguments.length; i++) {
total += arguments[i]
}
return total
}
- 未指定的类型参数默认为any
在extends语句中:使用JSDoc的@augments来明确地指定类型。
import { Component } from "react";
/**
* @augments {Component<{a: number}, State>}
*/
class MyComponent extends Component {
render() {
this.props.b; // Error: b does not exist on {a:number}
}
}
在JSDoc引用中:JSDoc里未指定的类型参数默认为any:
/** @type{Array} */
var x = [];
x.push(1); // OK
x.push("string"); // OK, x is of type Array<any>
/** @type{Array.<number>} */
var y = [];
y.push(1); // OK
y.push("string"); // Error, string is not assignable to number
在函数调用中
泛型函数的调用使用arguments来推断泛型参数。有时候,这个流程不能够推断出类型,大多是因为缺少推断的源;在这种情况下,类型参数类型默认为any。
var p = new Promise((resolve, reject) => { reject() });
p; // Promise<any>;
支持的JSDoc
@type {标记并引用一个类型名称}
@param (or @arg or @argument) {@param语法和@type相同,但增加了一个参数名。 使用[]可以把参数声明为可选的}
@returns (or @return) {函数的返回值类型也是类似的}
@typedef {可以用来声明复杂类型。 和@param类似的语法}
@callback 与@typedef相似,但它指定函数类型而不是对象类型
@template 使用@template声明泛型
@class (or @constructor)
编译器通过this属性的赋值来推断构造函数,但你可以让检查更严格提示更友好,你可以添加一个@constructor标记
@this {编译器通常可以通过上下文来推断出this的类型}
@extends (or @augments) {类继承了一个基类,指定类型参数的类型}
@enum {创建一个对象字面量,它的成员都有确定的类型}