一、基础类型
- 布尔值、数字、字符串
let isDone: boolean = false;
let decLiteral: number = 6;
let hexLiteral: number = 0xf00d;
let name: string = "bob";
let sentence: string = `Hello, my name is ${ name }.
- 数组
let list: number[] = [1, 2, 3];
let list: Array<number> = [1, 2, 3];
- 元组
let x: [string, number];
x = ['hello', 10]; // OK
x = [10, 'hello']; // Error
当访问一个越界的元素,会使用联合类型替代:
x[3] = 'world'; // OK, 字符串可以赋值给(string | number)类型
x[6] = true; // Error, 布尔不是(string | number)类型
- 枚举
enum Color {Red = 0, Green, Blue}
let c: Color = Color.Green;
转换为原生js为:
var color;
(function (color) {
color[color["red"] = 0] = "red";
color[color["green"] = 1] = "green";
color[color["blue"] = 2] = "blue";
})(color || (color = {}));
- Any
let notSure: any = 4;
notSure = "maybe a string instead";//ok
notSure = false; // ok
let list: any[] = [1, true, "free"];
list[1] = 100;
- Void
function warnUser(): void {
console.log("This is my warning message");
}
当一个函数没有返回值的时候,返回值类型是void,
void类型只能给他赋予undefined
和null
- Null和Undefined
TypeScript里,undefined和null两者各自有自己的类型分别叫做undefined和null
// Not much else we can assign to these variables!
let n: null = null;
let u: undefined = undefined;
- Never
never类型表示的是那些永不存在的值的类型。 never类型是那些总是会抛出异常或不会有返回值的函数的返回值类型; 变量也可能是 never类型,当它们被永不为真的类型保护所约束时。
只有never可以赋值给never,即使是any也不能给never赋值
// 抛出错误
function error(message: string): never {
throw new Error(message);
}
// 推断的返回值类型为never
function fail() {
return error("Something failed");
}
// 返回never的函数必须存在无法达到的终点
function infiniteLoop(): never {
while (true) {
}
}
- Object
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
- 类型断言
类型断言好比其它语言里的类型转换,但是不进行特殊的数据检查和解构。 它只是在编译阶段起作用。
//尖括号写法
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;
二、接口
- 可选属性
interface SquareConfig {
color?: string;
width?: number;//给他打个?
}
- 只读属性
interface Point {
readonly x: number;
readonly y: number;
}
let p1: Point = { x: 10, y: 20 };
p1.x = 5; // error!
//TypeScript具有ReadonlyArray<T>类型,它与Array<T>相似,只是把所有可变方法去掉了,因此可以确保数组创建后再也不能被修改:
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; //只读,赋值给别人也不行!
//如果想把ReadonlyArray赋值到一个普通数组,可以使用类型断言
a = ro as number[]; //ok
- 额外的类型检查
interface SquareConfig {
color?: string;
width?: number;
}
function createSquare(config: SquareConfig){
// ...
}
let mySquare = createSquare({ colour: "red", width: 100 });//error
对象字面量会被特殊对待而且会经过 额外属性检查,当将它们赋值给变量或作为参数传递的时候。 如果一个对象字面量存在任何“目标类型”不包含的属性时,你会得到一个错误。
- 绕过属性检查
//1.将这个对象字面量赋值给一个变量:因为变量不会经过额外属性检查
let squareOptions = { colour: "red", width: 100 };
let mySquare = createSquare(squareOptions);
//2.使用类型断言
let mySquare = createSquare({ colour: "red", width: 100 } as SquareConfig);
//3.字符串索引签名
interface SquareConfig {
color?: string;
width?: number;
[propName: string]: any;
}
- 函数类型
interface SearchFunc {
(source: string, subString: string): boolean;
}
let mySearch: SearchFunc;
mySearch = function(source: string, subString: string) {
let result = source.search(subString);
return result > -1;
}
- 可索引的类型
interface StringArray {
[index: number]: string;
}
let myArray: StringArray;
myArray = ["Bob", "Fred"];
let myStr: string = myArray[0];
可索引类型具有一个 索引签名,它描述了对象索引的类型,还有相应的索引返回值类型
ps:TypeScript支持两种索引签名:字符串和数字。 可以同时使用两种类型的索引,但是数字索引的返回值必须是字符串索引返回值类型的子类型。 这是因为当使用 number来索引时,JavaScript会将它转换成string然后再去索引对象。 也就是说用 100(一个number)去索引等同于使用"100"(一个string)去索引,因此两者需要保持一致
interface NumberDictionary {
[index: string]: number;
length: number; // 可以,length是number类型
name: string // 错误,`name`的类型与索引类型返回值的类型不匹配
}
//可以将索引签名设置为只读,这样就防止了给索引赋值
interface ReadonlyStringArray {
readonly [index: number]: string;
}
let myArray: ReadonlyStringArray = ["Alice", "Bob"];
myArray[2] = "Mallory"; // error!
- 用class实现接口
interface ClockInterface {
currentTime: Date;
}
class Clock implements ClockInterface {
currentTime: Date;
constructor(h: number, m: number) { }
}
接口只会检查类的公共部分,它不会帮你检查类是否具有某些私有成员
- 类的静态和实例部分
interface ClockConstructor {
new (hour: number, minute: number);
}
class Clock implements ClockConstructor {
currentTime: Date;
constructor(h: number, m: number) { }
}
//错误
当一个类实现了一个接口时,只对其实例部分进行类型检查。 constructor存在于类的静态部分,所以不在检查的范围内
应该先定义两个接口,一个是构造函数所用,一个是实例方法所用
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 PenStroke {
penWidth: number;
}
interface Square extends Shape, PenStroke {
sideLength: number;
}
let square = <Square>{};
square.color = "blue";
square.sideLength = 10;
square.penWidth = 5.0;
- 混合类型
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 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() { }
}
//在上面的例子中,只能够是Control的子类们才能实现SelectableControl接口
当接口继承一个类时,它会继承类的成员但不包括其实现。接口会继承到类的private和protected成员。 这意味着当你创建了一个接口继承了一个拥有私有或受保护的成员的类时,这个接口类型只能被这个类或其子类所实现(implement)
三、类
- 基本用法
class Greeter {
greeting: string;//指定greeting的类型
constructor(message: string) {
this.greeting = message;
}
greet() {
return "Hello, " + this.greeting;
}
}
let greeter = new Greeter("world");
我们声明一个 Greeter类。这个类有3个成员:一个叫做 greeting的属性,一个构造函数和一个 greet方法
- 继承
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); }//必须先调用super
move(distanceInMeters = 5) {
console.log("Slithering...");
super.move(distanceInMeters);
}
}
let sam = new Snake("Sammy the Python");
Snake是一个派生类,它派生自 Animal 基类,通过 extends关键字。 派生类通常被称作子类,基类通常被称作超类
- 公有public
在ts中,成员都默认为public,也可以显示地写为public - 私有private
class Animal {
private name: string;
constructor(theName: string) { this.name = theName; }
}
new Animal("Cat").name; // 错误: 'name' 是私有的.
可以在类内部调用函数获取到private成员
class Animal {
private x: string;
constructor(theName: string) { this.x= theName; }
getX(){
return this.x
}
}
console.log(new Animal("dog").getX()) //ok
当我们比较两种不同的类型时,并不在乎它们从何处而来,如果所有成员的类型都是兼容的,我们就认为它们的类型是兼容的
当我们比较带有 private
或 protected
成员的类型的时候,情况就不同了。 如果其中一个类型里包含一个 private
成员,那么只有当另外一个类型中也存在这样一个 private
成员, 并且它们都是来自同一处声明时,我们才认为这两个类型是兼容的。 对于 protected
成员也适用这个规则
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 不兼容.
这个例子中有 Animal和 Rhino两个类, Rhino是 Animal类的子类。 还有一个 Employee类,其类型看上去与 Animal是相同的。Animal和 Rhino共享了来自 Animal里的私有成员定义 private name: string,因此它们是兼容的,然而 Employee却不行
- protected
protected与 private的行为很相似,但是protected
成员在子类中仍然可以访问
class Person {
protected name: string;
constructor(name: string) { this.name = name; }
}
class Employee extends Person {
constructor(name: string) {
super(name)
}
public getElevatorPitch() {
return this.name;
}
}
let howard = new Employee("Howard");
console.log(howard.getElevatorPitch());//ok
console.log(howard.name); // 错误
构造函数也可以被标记成 protected, 这意味着这个类不能在包含它的类外被实例化,但是能被继承
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关键字将属性设置为只读的,只读属性必须在声明时或构造函数里被初始化
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 是只读的.
- 参数属性
class Octopus {
readonly numberOfLegs: number = 8;
constructor(readonly name: string) {
}
}
参数属性通过给构造函数参数前面添加一个访问限定符来声明。 使用 private限定一个参数属性会声明并初始化一个私有成员;对于 public和 protected来说也是一样
- 存取器
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);
}
注:必须将编译器设置为输出ECMAScript 5或更高
- 静态属性(指属性存在于类本身上面而不是类的实例上)
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);
}
constructor (public scale: number) { }
}
let grid1 = new Grid(1.0); // 1x scale
- 抽象类
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(); // 错误: 方法在声明的抽象类中不存在
- 把类当成接口使用
class Point {
x: number;
y: number;
}
interface Point3d extends Point {
z: number;
}
let point3d: Point3d = {x: 1, y: 2, z: 3};
四、函数
- 完整函数类型
let myAdd: (x: number, y: number) => number =
function(x: number, y: number): number { return x + y; };
- 推断类型
// 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; };
如果你在赋值语句的一边指定了类型但是另一边没有类型的话,TypeScript编译器会自动识别出类型,这叫做“按上下文归类”,是类型推论的一种
- 可选参数
在TypeScript里我们可以在参数名旁使用?
实现可选参数的功能,比如,我们想让last name是可选的
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
注意,可选参数必须在必须参数后面
- 默认参数
在TypeScript里,我们可以为参数提供一个默认值,当用户没有传递这个参数或传递的值是undefined时生效, 它们叫做有默认初始化值的参数
在所有必须参数后面的带默认初始化的参数都是可选的,与可选参数一样,在调用函数的时候可以省略。 也就是说可选参数与末尾的默认参数共享参数类型。
function buildName(firstName: string, lastName?: string) {
// ...
}
与
function buildName(firstName: string, lastName = "Smith") {
// ...
}
共享一个类型
(firstName: string, lastName?: string) => string
要注意的是,有默认值的参数可以写在参数的前面,当必须传入undefined
值来获得默认值
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"
- 剩余参数
function buildName(firstName: string, ...restOfName: string[]) {
return firstName + " " + restOfName.join(" ");
}
let buildNameFun: (fname: string, ...rest: string[]) => string = buildName;
- 重载
JavaScript里函数根据传入不同的参数而返回不同类型的数据是很常见的,但是这怎么在ts类型系统里表示呢?方法是为同一个函数提供多个函数类型定义来进行函数重载
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 };
}
}
注意,function pickCard(x): any并不是重载列表的一部分,因此这里只有两个重载:一个是接收对象另一个接收数字
五、泛型
- 泛型函数
function identity<T>(arg: T): T {
return arg;
}
定义了泛型函数后,可以用两种方法使用。 第一种是,传入所有的参数,包含类型参数;第二种方法更普遍。利用了类型推论,即编译器会根据传入的参数自动地帮助我们确定T的类型
let output = identity<string>("myString"); //第一种
let output = identity("myString"); // 第二种
类型推论帮助我们保持代码精简和高可读性。如果编译器不能够自动地推断出类型的话,只能像上面那样明确的传入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;
}
这可以让我们把泛型变量T当做类型的一部分使用,而不是整个类型,增加了灵活性
- 泛型类型
泛型函数的类型与非泛型函数的类型没什么不同,只是有一个类型参数在最前面
let myIdentity: <T>(arg: T) => T;//这就是一个泛型函数的类型写法
function identity<T>(arg: T): T {
return arg;
}
myIdentity = identity
现在我们去定义一个泛型接口
interface GenericIdentityFn {
<T>(arg: T): T;
}
function identity<T>(arg: T): T {
return arg;
}
let myIdentity: GenericIdentityFn = identity;
//也可以使用对象字面量的方式
let myIdentity: {<T>(arg: T): T} = identity;
同样,我们可以把泛型参数当作整个接口的一个参数,就知道使用的具体是哪个泛型类型
interface GenericIdentityFn<T> {
(arg: T): T;
}
function identity<T>(arg: T): T {
return arg;
}
let myIdentity: GenericIdentityFn<number> = identity;
- 泛型类
泛型类看上去与泛型接口差不多
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; };
泛型类指的是实例部分的类型,所以类的静态属性不能使用这个泛型类型
- 泛型约束
interface Lengthwise {
length: number;
}
//我们定义一个带有length属性的接口,让T继承这个接口实现泛型约束,
//这样传入的参数就必须带有length属性才正确
function loggingIdentity<T extends Lengthwise>(arg: T): T {
console.log(arg.length);
return arg;
}
- 在泛型中使用类类型
function create<T>(c: {new(): T; }): T {
return new c();
}
//或者
function create<T>(c: new() => T): T {
return new c();
}
六、枚举
- 数字枚举
enum Direction {
Up = 1,
Down,
Left,
Right
}
- 字符串枚举
enum Direction {
Up = "UP",
Down = "DOWN",
Left = "LEFT",
Right = "RIGHT",
}
字符串枚举每个成员都必须用字符串字面量,或另外一个字符串枚举成员进行初始化,字符串枚举没有自增长的行为
- 异构枚举
enum BooleanLikeHeterogeneousEnum {
No = 0,
Yes = "YES",
}
这样写语法上是可以的,但一般不会这样搞
- 计算的和常量成员
enum FileAccess {
// constant members 常量
None,
Read = 1 << 1,//2
Write = 1 << 2,//4
ReadWrite = Read | Write,//6
// computed member 计算的值
G = "123".length
}
- 高级枚举,枚举成员变成了类型
enum ShapeKind {
Circle,
Square,
}
interface Circle {
kind: ShapeKind.Circle;
radius: number;
}
interface Square {
kind: ShapeKind.Square;
sideLength: number;
}
let c: Circle = {
kind: ShapeKind.Square,// ~~~~~~~~ Error!
radius: 100,
}
- 反向映射
枚举类型被编译成一个对象,它包含了正向映射( name -> value)和反向映射( value -> name),不会为字符串枚举成员生成反向映射 const
枚举
常量枚举只能使用常量枚举表达式,并且不同于常规的枚举,它们在编译阶段会被删除,常量枚举不允许包含计算成员
const enum Directions {
Up,
Down,
}
let directions = [Directions.Up, Directions.Down]
//编辑成js之后为:
var directions = [0 /* Up */, 1 /* Down */];
七、类型兼容性
- 结构类型
interface Named {
name: string;
}
class Person {
name: string;
}
let p: Named;
// OK, because of structural typing
p = new Person();
//Named和Person具有相同的类型,所以不报错
在C#或Java中,这段代码会报错,因为Person类没有明确说明其实现了Named接口
interface Named {
name: string;
}
let x: Named;
// y's inferred type is { name: string; location: string; }
let y = { name: 'Alice', location: 'Seattle' };
x = y;
把y赋值给x,只需要检测x中的每个属性,y中是否也有,y中有就可以赋值。换句话说,y可以比x多一些属性
- 比较两个函数
let x = (a: number) => 0;
let y = (b: number, s: string) => 0;
y = x; // OK
x = y; // Error
要看x是否能赋值给y,首先看它们的参数列表。 x的每个参数必须能在y里找到对应类型的参数。 注意,参数的名字是否相同无所谓,只看类型
参数少的可以赋值给参数多的,而变量的赋值是属性多的可以赋值给属性少的,可以看出,函数的赋值和变量的赋值恰好相反
- 函数返回值
let x = () => ({name: 'Alice'});
let y = () => ({name: 'Alice', location: 'Seattle'});
x = y; // OK
y = x; // Error, because x() lacks a location property
仅仅是函数的返回值不同的函数赋值,跟变量的赋值有一些类似
- 比较类
class Animal {
feet: number;
constructor(name: string, numFeet: number) { }
}
class Size {
feet: number;
constructor(numFeet: number) { }
}
let a: Animal;
let s: Size;
a = s; // OK
s = a; // OK
类与对象字面量和接口差不多,但有一点不同,类有静态部分和实例部分的类型。 比较两个类类型的对象时,只有实例的成员会被比较。 静态成员和构造函数不比较
类的私有成员和受保护成员会影响兼容性,当检查类实例的兼容时,如果目标类型包含一个私有成员,那么源类型必须包含来自同一个类的这个私有成员,这条规则也适用于受保护成员实例的类型检查
- 比较泛型
interface Empty<T> {
}
let x: Empty<number>;
let y: Empty<string>;
x = y; // OK, because y matches structure of x
//换一下就不一样了
interface NotEmpty<T> {
data: T;
}
let x: NotEmpty<number>;
let y: NotEmpty<string>;
x = y; // Error, because x and y are not compatible
//再换一下,对于没有指定指定泛型类型的参数会被当成any
let identity = function<T>(x: T): T {
// ...
}
let reverse = function<U>(y: U): U {
// ...
}
identity = reverse; // OK, because (x: any) => any matches (y: any) => any