TypeScript
1、基础
基础类型
布尔值
let isTrue: boolean = false;
数字
typescript不仅支持十进制和十六进制还支持二进制和八进制
let num1: number = 6;
let num2: number = 0xf00d;
字符串
let name: string = 'lisi';
name = "zhangsan";
name = 4;//声明过类型的变量不能转化为别的类型,会报错
还可以使用模板字符串
let name: string = 'lisi';
let age: number = 37;
let newstring: string = `这是模板字符串 ${name}`;
//打印'这是模板字符串lisi'
let string2: string = `这是模板字符串${age + 1}`;
//打印'这是模板字符串38'
console.log(newstring);
console.log(string2);
数组
typescript定义数组的方法
第一种,在元素类型后面加上[ ],
let newarr: number[] = [1,2,3];//声明这是一个只有number类型的数组
第二种方法,使用数组泛型,Array<元素类型>。
let list: Array<number> = [1,2,3];
//声明了list是一个只有number类型的数组
元组
元组的作用是让已知元素数量和类型的数组各个元素的类型不必相同,也就是说可以让声明的数组拥有不同的元素类型。
let tuple: [string,number];//声明一个元组,拥有string和number元素类型。并且要一一对应。比如
tuple = ['hi',1];//success
tuple = [1,'hi'];//error
获取已知索引的函数,可以得到正确的类型,可以使用该类型的方法。
console.log(tuple[0].substr(1));//可以使用字符串截取
console.log(tuple[1].substr(1));//不是字符串类型不可以使用字符串的方法。
访问越界元素会用联合类型代替
let tuple:[string,number];
tuple[3] = 'world';//官方文档说没问题,本地会报错,待定。
枚举
enum类型,对javascript标准数据类型的补充。枚举类型可以为一组数值取名字。
enum Color {Red, Green, Blue};//一组数值默认从0开始,也就是说Red是0,Green是1。
let c: Color = Color.Green;
console.log(c);//打印结果为1
也可以给手动指定成员的值。
enum Color {Red = 1, Green, Blue};//Red赋值之后从1开始
let c: Color = Color.Green
console.log(c);//打印2
enum Color {Red, Green = 2, Blue};//Green赋值之后从2开始依次递增
let c: Color = Color.Red
let x: Color = Color.Blue
console.log(c);//打印0
console.log(x);//打印3
还可以使用枚举的值获得枚举的名字。
enum Color {Red, Green, Blue};
let newname: string = Color[2];
console.log(newname);//Blue
Any
可以对不清楚类型的变量指定一个变量类型。先使用any类型标记这些变量。
let notstr: any = 4;//any类型可以随意的切换类型。
notstr = '22222';
notstr = false;
console.log(notstr);//false
object有类似的方法,但是区别是object只能赋值不能随意的调用方法。
let notstr: any = 4;
notstr.toFixed();//可以使用
let objectstr: object = 4;
objectstr.toFixed();//不可以使用
any类型和数组组合使用。
let list: any[] = [1,'2',false];//可以随意放入数据
list[1] = 100;//可以随意赋值
Void
void表示没有任何类型。void类型只能赋值undefined和null。
let unusable: void = undefined;
Null 和 Undefined
undefined和null又有自己的类型分别叫undefined和null。
let a:undefined = undefined;
let b:null = null;
Never
Never类型表示的是那些永不存在的类型,比如抛出异常,或者根部不会有返回值的函数表达式或者箭头函数表达式。Never类型是任何类型的子类型,也可以赋值给任何类型。没有类型是Never类型的子类型或者可以赋值给never类型除了自己本身外。
//抛出异常的函数
function error(message:string): never {
throw new Error(message)
}
//推断返回值为never
function loop() {
return error('Something failed');
}
//返回never的函数必须存在无法到达的终点。
function infin(): never {
while (true) {
}
}
Object
declare function create(o: object | null): void {
console.log(o);
};
create({p:0})//success
create(null)//success
create(42)//error
create('234')//error
create(false)//error
create(undefined)//error
类型断言
当你清楚的知道一个类型比当前类型更加确切的类型时候,可以通过类型断言的这种方式告诉编译器“相信我,我知道自己在干什么“并且不进行报错处理。
类型断言有两种方式。
let someValue: any = 'This is a string';
let strLength: number = (<string>someValue).length;//第一种,尖括号方式。
let striLength: number = (someValue as string).length;
//第二种,as方式。建议使用。
接口(interface)
function print1 (lableobj: {lable: string}) {
console.log(lableobj.lable);
}
let myobj = {size:10,lable: 'white'};
print1(myobj);//‘white’
使用接口去写,接口相当于一个名字,用来描述上面例子里面的要求。
interface lablestyle {
lable: string;
}
function print1 (lableobj: lablestyle) {
console.log(lableobj.lable);
}
let myobj = {size:10,lable: 'white'};
print1(myobj);//‘white’
可选属性
interface Sqconfig {
color: string;
width: number;
}
function createsq (config: Sqconfig): {color:string;area: number} {
//声明类型既不是void也不是any的函数必须有返回值,也就是return,否则会报错
let newsq = {color:'white',area:100};
if(config.color) {
newsq.color = config.color;
}
if(config.width) {
newsq.area = config.width * config.width
}
return newsq
}
// let mySquare = createsq({a:1});//error
let mySquare = createsq({color:'white'});//error,缺少一个属性。
console.log(mySquare);
接口里面的属性并不是全部都是必须的,我们可以使用可选属性进行部分属性的赋值。
//第一种,必选,传入参数时两个属性为必填项。
interface Sqconfig {
color: string;//可选属性,在名字后面加上?
width: number;
}
//第二种,color为可选项,width为必填项
interface Sqconfig {
color?: string;//可选属性,在名字后面加上?
width: number;
}
//第三种,当两个都为可选项的时候,传入的参数必须有其中一个,否则会报错。
interface Sqconfig {
color?: string;//可选属性,在名字后面加上?
width?: number;
}
function createsq (config: Sqconfig): {color:string;area: number} {//声明类型既不是void也不是any的函数必须有返回值,也就是return,否则回报错
let newsq = {color:'white',area:100};
if(config.color) {
newsq.color = config.color;
}
if(config.width) {
newsq.area = config.width * config.width
}
return newsq
}
// let mySquare = createsq({a:1});//error
let mySquare = createsq({color:'white'});
console.log(mySquare);
let一个obj传入参数,会检查obj里面是否有所需要的参数,如果在函数的参数上直接传入,则不能有其他的参数。
let mySquare = createsq({color:'white'});//success
let mySquare = createsq({color:'white',size:100});//error
let obj = {color:'white',size:100};
createsq(obj)//success
console.log(mySquare)
只读属性
一些对象属性只能在创建的时候修改他的属性,可以在属性名前面使用readonly来指定只读属性。
interface Point {
readonly x: number;
readonly y: number;
}
然后我们通过赋值一个对象构造一个Point,x和y就不能再改变了。
let p1: Point = {x:10,y:10};
p1.x = 5;//error
Typescript具有ReadonlyArray类型,跟Array类似,只是把可变的方法去掉了。
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!
最后一行,把数组直接赋值到一个普通数组也不可以。但是我们可以使用类型断言重写。
a = ro as number[];
readonly和const的区别
判断该使用readonly还是const的方法是作为一个属性还是作为一个变量,变量的话使用const ,属性则为readonly。
额外的属性检查
interface Sqconfig {
color?: string;//可选属性,在名字后面加上?
width?: number;
[propName: string]: any;
}
function createsq (config: Sqconfig): {color:string;area: number} {//声明类型既不是void也不是any的函数必须有返回值,也就是return,否则回报错
let newsq = {color:'white',area:100};
if(config.color) {
newsq.color = config.color;
}
if(config.width) {
newsq.area = config.width * config.width
}
console.log(config.size);
return newsq
}
let mySquare = createsq({ colour: "red", width: 100 });//error!
这个例子中,我们可以看到传入的参数是colour而不是color,绕开检查的方法是使用类型断言。
let mySquare = createsq({ width: 100, size: 0.5 } as SquareConfig);
但是类型断言的坏处是不能在函数中访问到别的属性。
function createSquare(config: SquareConfig): { color: string; area: number } {
console.log(config.size);//error
}
最好的方法就是在接口添加一个索引签名。
interface Sqconfig {
color?: string;//可选属性,在名字后面加上?
width?: number;
[propName: string]: any;
}
function createsq (config: Sqconfig): {color:string;area: number} {
let newsq = {color:'white',area:100};
if(config.color) {
newsq.color = config.color;
}
if(config.width) {
newsq.area = config.width * config.width
}
console.log(config.size);//success
return newsq
}
let mySquare = createsq({size:100,width:100});
函数类型
定义一个接口,创建一个函数类型得变量,
interface serarchface {
(sub: string, str: string): boolean
}
let myserach: serarchface;
myserach = function(sub: string,str: string) {
//函数没有写返回值得类型类型检查会通过返回值推断出来,检查是否与接口得返回值类型相匹配。
let res = sub.search(str);
return res > -1;
}
myserach = function(sub: string,str: string): boolean {
let res = sub.search(str);
return res > -1;
}
可索引的类型
可索引类型具有一个索引签名,描述了对象的索引签名还要相应的返回类型。
interface StringArray {
[index: number]: string;//设置索引类型为number,适用于数组类型,值为string,索引类型为string适用于对象。
}
let myArray: StringArray;
myArray = ["Bob", "Fred"];
let myStr: string = myArray[0];
interface num {
[index: string]: number;//索引类型为string的时候,适用于object类型,当索引类型为number的时候适用于Array类型。
length:number;
name1: number;
}
let arr: num = {length:10,name1:9}
也可以设置只读属性
interface num {
readonly [index:number]: string;
}
let myarr: num = ["white","bolb"];
myarr[2] = "red";//error
类类型
和c#和java里面的接口一样,typescript也能够用它来明确的强制一个类去符合某种契约。
interface Clockinter {
currentTime: Date;
setTime(d: Date);
}
class Clock implements Clockinter {
currentTime: Date;
setTime(d: Date) {
console.log(d);
}
constructor(h: number, m: number) { }
}
表示声明自己使用一个或者多个接口。类Colck必须得有currentTime和setTime(d: Date)
类静态部分和实例部分的区别
首先要知道类的静态部分和实例部分。
当我们清楚静态部分和实例部分去实现一个构造器的接口,构造器是静态类型的接口,会报错。
类不能直接去实现静态部分的接口,但是可以实现实例部分的接口。
interface ColckConstructor {
new (hour: number,minute: number);
}
class Colck implements ColckConstructor {
currentTime: Date;
constructor(h: number, m: number) { }
}
也就是说即使我们的接口写了静态部分的检查,但是一个类在类型检查也只会对实例部分进行检查。
如果我们需要对静态部分进行检查。我们就不能在类里面进行类型检查。需要在构造函数里面进行。
//定义静态类型接口
interface ClockConstructor {
new (hour: number, minute: number);
}
//定义实例部分的接口
interface ClockInterface {
tick();
}
//创建一个函数
function create (ctor: ClockConstructor) {
}
//定义一个类
class DigitalClock implements ClockInterface {
constructor(h: number, m: number) { }
tick() {
console.log("beep beep");
}
}
create(DigitalClock)
我们可以看到ClockConstructor(静态部分接口)被构造函数create所用,而ClockInterface(实例部分接口)被定义的类DigitalClock所用,这样就会对类DigitalClock进行静态部分的类型检查。只能对
接口继承
和类一样,接口之间可以相互继承
interface Shap {
color: string;
}
interface Square extends Shap {
num: number
}
let sq = <Square>{}
sq.color = "blue";
sq.num = 10;
一个接口也可以继承多个接口
// 接口继承
interface Shap {
color: string;
}
interface cap {
list: string;
}
interface Square extends Shap,cap {
num: number
}
let sq = <Square>{}
sq.color = "blue";
sq.num = 10;
sq.list = '2222'
混合类型
一个对象可以同时作为函数和对象使用。
interface Counter {
(start: number): string;
interval: number;
reset(): void;
}
//定义一个接口,拥有函数类型和对象。
function gp (): Counter {
//非void和any的函数需要返回一个值,满足Counter
let res =<Counter> function(start: number): string{
return String(start)
}
res.interval = 10;
res.reset = function(){
}
console.log(res);
return res
}
let c = gp()
c(10);
console.log(c);
接口继承类
接口继承类类型时候,会继承类的成员但是不包括实现。
//定义一个类
class Control {
private state: any;
}
//接口继承类,这个接口类型只能被这个类或者其子类实现。
interface SelectableControl extends Control {
select(): void;
}
//类继承了Control的类,可以实现接口
class Button extends Control implements SelectableControl {
select() { }
}
//类继承了Control类,可以实现接口
class TextBox extends Control {
select() { }
}
// 错误:“Image”类型缺少“state”属性。
//image不是Control的子类,不能实现接口。
class Image implements SelectableControl {
select() { }
}
class Location {
}
类
类
class Animal {
//基类
name: string;
constructor (thename: string) {
this.name = thename;
}
move (distance: number = 0) {
console.log(`${this.name} + ${distance}`);
}
}
//派生类,也就是继承了别的类的类,其构造函数constructor必须调用super
class Snake extends Animal {
constructor(nametwo: string) {
super(nametwo)
}
move(distance: number = 5) {
super.move(distance)
}
}
let sam = new Snake('666')
// 创建snake实例,传参666,Snake的构造函数执行,调用super传参666,Animal的构造函数执行,Animal的name赋值666
sam.move();//666 + 5
// Snake的实例move被调用,默认值5,super调用Animal的move实例,传参默认值5,执行打印
公共私有受保护的的修饰符
默认为public
public指定成员是可见的。在typescript里面成员都是默认为public。
理解private
当成员被标记为private的时候,他就不能再声明他的类的外部访问。
class Animal {
private name: string;
constructor(theName: string) { this.name = theName; }
}
//实例化之后不能访问到Animal的实例。
new Animal("Cat").name; // 错误: 'name' 是私有的.
一般我们对比两种不同的类型的时候,只要所有类型都是兼容的,我们就认为他们时兼容的。
当我们比较带有private的成员的类型的时候,只有两个类型里面都要带有这样的一个private成员,并且都来自同一处声明,这两个类才是兼容的。
1、都带有private。
2、来自同一处声明。
class Animal {
private name: string;
constructor (num: string) {
this.name = num;
}
}
class Square extends Animal {
constructor(){
super('Square');
}
}
class Emp {
private name: string;
constructor (thename: string) {
this.name = thename;
}
}
let animal = new Animal('gg');
let square = new Square();
let emp = new Emp('ggg')
animal = square;//因为square是继承于Aniaml类。
animal = emp;//即使拥有一样的private,但是不是来自同一处声明,也不可以兼容。
理解protected
protected修饰符和private修饰符类似,但是protected修饰符在派生类中也可以访问
在private中,派生类是访问不到基类被private标记的成员。
class Animal {
private name: string;
constructor (thename: string) {
this.name = thename
}
num:10
private move(){
console.log(this.num);
return this.num
}
}
class Square extends Animal {
constructor () {
super('8888')
}
moo = super.num;//只有基类的“方法”可以被supuer调用
moo1 = super.name;//error,super只能访问基类的方法,且派生类访问不到基类被private标记过的成员。
moo2 = super.move()//error,派生类访问不到基类被private标记过的成员。
}
let abc = new Square()
console.log(abc.moo2);
protected和private类似但是唯一的不同点就是派生类也能访问到基类的成员。
class Animal {
protected name: string;
constructor (thename: string) {
this.name = thename
}
num:10
protected move(){
console.log(this.num);
return this.num
}
}
class Square extends Animal {
constructor () {
super('8888')
}
moo = super.num;//只有基类的“方法”可以被supuer调用
moo1 = super.name;//error,super只能访问基类的方法,且派生类访问不到基类被private标记过的成员。
moo2 = super.move()//可以访问基类的protected标记过的成员。
}
let abc = new Square()
console.log(abc.moo2);
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 是只读的.
参数属性
参数属性的作用就是可以让我们在一个地方定义并且初始化一个成员,避免冗余的代码。
下面的例子就是修改上面的例子。直接在构造函数上面添加参数属性,需要在参数前面添加一个访问限定符来声明,比如private、public、protected、readonly。
class Octopus {
readonly numberOfLegs: number = 8;
constructor(readonly name: string) {
}
}
let opt = new Octopus('666')
console.log(opt.name);
存取器
ts支持使用getters和setters截取对对象成员的访问。
let passcode = "secret passcode";
class Employee {
private _fullName: string;
//访问时生效
get fullName(): string {
console.log(1);
return this._fullName;
}
//在进行参数的修改的时候生效
set fullName(newName: string) {
console.log(2);
if (passcode && passcode == "secret passcode") {
this._fullName = newName;
}
else {
console.log("Error: Unauthorized update of employee!");
}
}
}
let employee = new Employee();
employee.fullName = "Bob Smith";//对实例进行赋值,执行set
if (employee.fullName) {//访问了实例,执行get
alert(employee.fullName);//访问了实例,执行get
}
静态属性
我们也可以创建类的静态成员,我们使用static定义静态成员origin,跟实例不一样的是我们不能通过实例化访问到这个属性,要直接通过类来访问静态属性。
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}));
console.log(grid1.orgin);//不能通过实例化访问静态属性。
console.log(Grid.orgin);//可以通过类直接访问静态属性。
抽象类
抽象类要作为其他派生类的基类使用,抽象类一般不能直接实例化,被实例化的派生类不能访问派生类的实例,只能访问抽象类的实例方法。
abstract class Department {
constructor(public name: string) {
}
printName(): void {
console.log('Department name: ' + this.name);
}
abstract printMeeting(): void; //被abstract的成员必须在派生类中实现
}
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 Greeter {
greeting: string;
constructor(message: string) {
this.greeting = message;
}
greet() {
return "Hello," + this.greeting;
}
}
let greeter: Greeter;//此处声明的意思是greeter类的实例的类型是Greeter。
greeter = new Greeter('666');
console.log(greeter.greet());
当他们被编译成js之后。
let Greeter = /** @class */ (function () {
//静态部分
function Greeter(message) {
this.greeting = message;
}
//实例部分
Greeter.prototype.greet = function () {
return "Hello," + this.greeting;
};
return Greeter;
}());
let greeter;
greeter = new Greeter('666');
console.log(greeter.greet());
let Greeter 将被赋值为构造函数,当我们调用new并执行了这个函数之后,会得到一个类的实例,这个实例包含了所有的静态属性,也就是说我们可以认为类具有实例部分和静态部分两个部分。
class Greeter {
static standardGreeting = "Hello, there";
greeting: string;
greet() {
if (this.greeting) {
return "Hello, " + this.greeting;
}
else {
return Greeter.standardGreeting;
}
}
}
let greeter1: Greeter;
greeter1 = new Greeter();
console.log(greeter1.greet());
let greeterMaker: typeof Greeter = Greeter;
greeterMaker.standardGreeting = "Hey there!";
let greeter2: Greeter = new greeterMaker();
console.log(greeter2.greet());
编译后
var Greeter = /** @class */ (function () {
function Greeter() {
}
Greeter.prototype.greet = function () {
if (this.greeting) {
return "Hello, " + this.greeting;
}
else {
return Greeter.standardGreeting;
}
};
Greeter.standardGreeting = "Hello, there";
return Greeter;
}());
var greeter1;
greeter1 = new Greeter();
console.log(greeter1.greet());
var greeterMaker = Greeter;
greeterMaker.standardGreeting = "Hey there!";
var greeter2 = new greeterMaker();
console.log(greeter2.greet());
再之后,我们直接使用类。 我们创建了一个叫做 greeterMaker
的变量。 这个变量保存了这个类或者说保存了类构造函数。 然后我们使用 typeof Greeter
,意思是取Greeter类的类型,而不是实例的类型。 或者更确切的说,“告诉我 Greeter
标识符的类型”,也就是构造函数的类型。 这个类型包含了类的所有静态成员和构造函数。 之后,就和前面一样,我们在 greeterMaker
上使用 new
,创建 Greeter
的实例。
函数
函数
js中有两种函数
//命名式函数
function add(x, y) {
return x + y;
};
//声明式函数
let myadd = function(x, y) {
return x + y;
};
js中,可以使用外部的变量
let z = 100;
function add(x, y) {
return x + y + z;
}
函数类型
给函数定义类型
//命名式函数
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;
}
函数类型包含两部分:参数类型和返回值类型。
参数类型:x:number ,y: number。
返回值类型:number;
需要写完整的函数类型两者都是必须的,
参数列表形式:
let myadd: (baseValue: number,increment:number) =>number
当我们以参数列表的形式写出参数类型,为每一个参数指定一个名字和类型,名字是为了增加可读性。
let myAdd: (baseValue: number, increment: number) => number =
function(x: number, y: number): number { return x + y; };
参数类型一致即可,参数名可随意。
返回值类型
对于返回值我们在函数和返回值类型之前使用=>会比较清晰。函数必须有返回值类型,如果函数没有返回值须定义返回值类型为void;
推断类型
当我们赋值时只有一边写了类型而另一边没有写类型的话,ts会自动识别类型。
// myAdd has the full function typemyAdd具有完整的函数类型
let myAdd = function(x: number, y: number): number { return x + y; };
// The parameters `x` and `y` have the type number,参数'x'和'y'具有类型;
let myAdd: (baseValue: number, increment: number) => number =
function(x, y) { return x + y; };
可选参数和默认参数
在ts中每个参数都是必须的,也就是编译器会检查用户是否为每个参数传值;
function build(a: string,b: string) {
return a + "" + b;
}
let res = build('222');//error
let res = build('222','333');//success
let res = build('222','333','4444');//error
js中每个参数都是可选的,不传参默认undefined。
在ts中可以在参数名旁边使用?实现可选参数功能。可选参数必须放在必须参数的后面
function build(a: string,b?: string) {
return a + "" + b;
}
let res1 = build('222');//success
let res2 = build('222','333');//success
let res3 = build('222','333','4444');//error
ts中还可以提供参数的默认值。
function build(a: string,b: string = 'smit') {
return a + "" + b;
}
let res1 = build('222');//success returns "222 smit"
let res2 = build('222','333');//success
let res3 = build('222',undefined);//当参数为undefined的时候,相当于没有传参,returns "222 smit"
默认参数时可选的,在调用参数的时候可以只保留可选参数或者默认参数。
虽然默认参数拥有和可选参数一样的特性,但是默认参数可以放在必须参数前面。
但是需要注意的是放在前面必须要传入undefined才能获得默认值。
function build(b: string = 'smit',a: string) {
return a + "" + b;
}
let res1 = build('222');//error,必须参数没传参
let res2 = build('222','333');//success
let res3 = build(undefined,'222');//当参数为undefined的时候,相当于没有传参,returns "smit 222"
剩余参数
当我们想要同时操作多个值的时候,或者说你并不知道有多少参数传进来的时候,在js中可以使用arguments(隐式参数)来访问所有传入的参数。
但是在ts中可以把所有参数收集到一个变量里面,剩余参数会被当作数个可选参数,可以一个都没有
function build(a: string, ...rest: string[]) {
return a + " " + rest.join(" ");
}
let a = buildName("Joseph", "Samuel", "Lucas", "MacKinzie");
对剩余参数进行类型定义
let myadd:(x: number,...rest: string[]) => string = build
this
js中的this只有函数调用的时候才会指定。
let deck = {
suits: ["hearts", "spades", "clubs", "diamonds"],
cards: Array(52),
createCardPicker: function() {
return function() {
let pickedCard = Math.floor(Math.random() * 52);
let pickedSuit = Math.floor(pickedCard / 13);
return {suit: this.suits[pickedSuit], card: pickedCard % 13};
}
}
}
createCardPicker返回了一个函数,调用的时候这个返回的函数里面的this指向的是window而不是deck对象。
解决这个问题我们可以用箭头函数,箭头函数能保存函数创建的时候this的值,而不是调用的时候的值。不过ts会警告报错,this的类型为any
return ()=> {
let pickedCard = Math.floor(Math.random() * 52);
let pickedSuit = Math.floor(pickedCard / 13);
return {suit: this.suits[pickedSuit], card: pickedCard % 13};
}
}
this参数
我们可以提供一个显式的this参数,this参数是假的参数。如下例子
interface Card {
created(this: Card ): ()=> number
//此处的声明为created函数,参数为this: Card,返回值类型为()=> number,()=> number为一个函数返回值为number
//设置为this不用传参,设置为其他值需要传参
//例如a:Card则需要传一个类型为Card的参数
}
let res: Card = {
created: function (this: Card) {
console.log(this);
//函数返回值为 () => number
return () => {
//返回值为number
return 1
}
}
}
console.log(res.created());
this参数在回调函数里
先看下面的例子
未使用this参数
class Rectangle {
private w: number;
private h: number;
constructor(w: number, h: number) {
this.w = w;
this.h = h;
}
getArea() {
return () => {
return this.w * this.h;
};
}
}
在这个方法中我们没有使用this参数,此时,这个地方的this的类型就是this。
使用this参数
class Rectangle {
private w: number;
private h: number;
constructor(w: number, h: number) {
this.w = w;
this.h = h;
}
getArea(this: Rectangle) {
return () => {
return this.w * this.h;//此处的this是Reactangle
};
}
}
使用了this参数之后,this的类型就是Reactangle。
禁止使用this
有时候我们希望在方法中禁止用户使用this,针对这种需求可以将this的类型设置未void。
class Rectangle {
private w: number;
private h: number;
constructor(w: number, h: number) {
this.w = w;
this.h = h;
}
getArea(this: void) {
return () => {
return this.w * this.h;//error,不可以使用this
};
}
}
回调函数中的this
当我们在回调函数里面使用this的时候,这个地方使用this的隐式具有的式any类型。因为他没有类型注解。
const button = document.querySelector("button");
// ?. -> TS 3.7引入的可选链
button?.addEventListener("click", handleClick);
function handleClick() {
console.log("Clicked!");
// 'this' implicitly has type 'any' because it does not have a type annotation.
this.removeEventListener("click", handleClick);
}
所以我们要指定显示的this参数类型。
const button = document.querySelector("button");
button?.addEventListener("click", handleClick);
function handleClick(this: HTMLElement) {
console.log("Clicked!");
this.removeEventListener("click", handleClick);
}
重载
当我们有一个方法需要传入不同类型得参数返回不同得类型得时候,我们可以定义一个函数提供多个函数类型进行函数得重载
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 {
// Check to see if we're working with an object/array
// if so, they gave us the deck and we'll pick the card
if (typeof x == "object") {
let pickedCard = Math.floor(Math.random() * x.length);
return pickedCard;
}
// Otherwise just let them pick the card
else if (typeof x == "number") {
let pickedSuit = Math.floor(x / 13);
return { suit: suits[pickedSuit], card: x % 13 };
}
}
当我们调用函数得时候会进行类型检查,他会查找重载列表,尝试使用第一个重载定义。所以定义重载得时候一定要把最精确得定义放在最前面
泛型
function idengtity(arg: number): number {
retrun arg
}
也可以使用any类型
function identity(arg: any): any {
return arg
}
但是any类型会导致这个函数可以传入任何的参数
所以我们需要使返回值和传入的参数类型是相同的,要使用类型变量
function identity<T>(arg: T) : T {
return arg
}
给函数添加了类型变量,会捕获我们传入的类型。我们就可以使用这个类型。例如
function identity<T>(arg:T): T {
return arg
}
let output = identity<string>('string')
console.log(output);//string
//我们不一定要用<T>来明确传入的类型
let output = identity('string');//编译器会自动识别传入的参数的类型。
当我们需要传入的参数和返回参数为特定类型的时候只需要更改类型变量即可。
使用泛型变量
比方说我们要使用上面的例子打印长度。
function identity<T>(arg: T): T {
console.log(arg.length);//Property 'length' does not exist on type 'T'.
//类型T上面不存在属性length
return arg
}
编译器会报错说属性length不存在于类型T上面。如果我们操作的是T类型的数组而不是T
function identity<T>(arg: Array<T>): Array<T> {
console.log(arg.length);
return arg
}
可以发现不会报错了,泛型函数identity接受了类型变量T和参数arg,arg的类型是类型为T的数组,返回一个类型为T的数组
泛型类型
泛型的函数类型和非泛型的函数类型没有什么不同,只是添加了一个类型参数在最前面。
function identity<T>(arg: T): T {
return arg;
}
let myIdentity: <T>(arg: T) => T = identity;
我们可以写泛型接口
interface Card {
<T>(arg: T): T
}
function identity<T>(arg: T): T {
return arg
}
let output: Card = identity
我们也可以把泛型参数当作整个接口的参数,可以更加的清楚我们使用了的是那个泛型的类型
interface Card<T> {
(arg: T): T
}
function identity<T>(arg: T): T {
return arg
}
let output: Card<number> = identity;
泛型类
和接口的使用方法差不多
class Card<T> {
zero: T;
add: (x: T, y: T) => T;
}
let ident = new Card<string>()
ident.zero = 'ssss';
ident.add = function (x, y) {
return x
}
要注意的是泛型类值得是实例部分的类型,所以静态部分不能使用这个泛型类型。
泛型约束
我们的第一个例子访问不到length属性,所以我们要定义一个接口来描述和约束条件,创建一个包含length属性的接口,通过继承这个接口实现类型参数带有这个属性。
interface Lengthwise {
length: number;
}
function identity<T extends Lengthwise>(arg: T): T {
console.log(arg.length);
return arg
}
这样,我们就可以设置只有传入的参数具有.length属性参可以传参,这样就能访问到.length属性,不会报错。
泛型使用类类型
当我们使用typescript创建工厂函数的时候,需要引用构造函数的类类型。
function create<T>(c: {new(): T; }): T {
return new c();
}
class Card {
indent: string;
constructor(x){
this.indent = x
}
}
function identity(arg: new () => string) {
//此处的arg: new () => string参数指的是一个函数类型,参数是一个类类型,返回值是一个string类型。
return arg
}
let p = new Card('123')
function pppp(p: Card): string {
return p.indent
}
pppp(new Card('123'))
identity(pppp(p));
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();
}
//new () => A这个参数的意思是需要传入一个构造函数
createInstance(Lion).keeper.nametag; // typechecks!
createInstance(Bee).keeper.hasMask; // typechecks!
枚举
数字枚举
enum ident {
one = 1,
two,
three
}
console.log(ident.one);//1
console.log(ident.two);//2
console.log(ident.three);//3
这里定义了一个数字枚举,one使用初始化1,其他成员会依次增长。如果不使用初始化器,那么就会从0开始增加。
使用枚举
enum ident {
yes = 0,
no = 1
}
function getOneT(message: ident) {
console.log(message);
}
getOneT(ident.yes)
数字枚举可以混入到计算过的和常量成员后面。
字符串枚举
字符串枚举每个成员都必须用字符串变量或者另外一个枚举成员进行初始化。
enum ident {
one = '123',
two = '321',
three = two
}
console.log(ident.three);//321
异构枚举
也就是枚举混合成员。
enum ident {
one = 0,
two = '123'
}
不建议
计算的和常量成员
枚举的第一个成员且没有初始化,默认被赋值为0。
enum ident {
x
}
有初始值的话回依次递增
枚举成员使用常量枚举表达式初始化,当一个表达式满足下面条件之一的时候他就是一个常量枚举表达式。
- 一个枚举表达式字面量(字符串字面量或者数字字面量)
- 一个对之前定义的常量枚举成员的引用。
- 带括号常量枚举表达式。
- 一元运算符+,-,~其中之一应用再来常量枚举表达式
- 常量枚举表达式做为二元运算符
+
,-
,*
,/
,%
,<<
,>>
,>>>
,&
,|
,^
的操作对象。 若常数枚举表达式求值后为NaN
或Infinity
,则会在编译阶段报错。
联合枚举与枚举成员类型
字面量枚举成员,字面量枚举成员是指不带有初始值的常量枚举成员或者是值被初始化为
- 任何字符串字面量(例如:
"foo"
,"bar"
,"baz"
); - 任何数字字面量(例如:
1
,100
); - 应用了一元
-
符号的数字字面量(例如:-1
,-100
);
当所有成员都是字面量枚举的时候,枚举成员就成为了类型。
enum ident {
one,
two
}
enum ident1 {
one,
two
}
interface Crile {
kind: ident.one
}
let c: Crile = {
kind: ident1.one // error
}
运行时的枚举
枚举在运行的时候是一个真实存在的对象
enum E {
X, Y, Z
}
function f(obj: { X: number }) {
return obj.X;
}
f(E);
//可以使用,因为E在运行的时候是一个对象斌且对象里面有x属性而且类型是number
反向映射
反向映射的例子
enum ident {
A
}
let a = ident.A;
let nameof = ident[a];
console.log(nameof);//A
可以用枚举的值反向映射出枚举的名字,不能对字符串枚举成员生成反向映射。
const枚举
const enum高效的编译时内联。
官方文档明确写出“大多数情况下,枚举是十分有效的方案。 然而在某些情况下需求很严格。 为了避免在额外生成的代码上的开销和额外的非直接的对枚举成员的访问,我们可以使用 const枚举”,那是为什么呢?
那是因为通过const enum定义的编译时枚举类型,效果和通过C/C++的#define定义常量没实质区别。说白了就是假如仅仅通过通过const enum定义了枚举类型而没有其它地方调用,这段代码将在编译时被直接抹掉。
当其它地方调用该枚举类型时,将直接把枚举类型成员的值内联到使用处,如下:
const enum Response {
No,
Yes,
}
console.log(Response.NO, Response.Yes);//0,1
什么时候用enum?又在什么场景下用const enum呢?
先说说结论:
使用enum的场景:
1.1. 需要使用反向映射时;
1.2. 需要编译后的JavaScript代码保留对象.属性或对象[属性]形式时。
使用const enum的场景:能不用enum时就用const enum(哈哈!)
使用enum的场景中的第一条还很好理解,但第二条是啥回事呢?我这里有个真实发生的示例,可以让大家更好的理解:
背景:为Photoshop的ExtendScript编写类型声明。
需求:DialogModes.NO在ExtendScript中返回值为DialogModes.No本身,编译后的JavaScript中必须保留DialogModes.NO的代码形式。
那么又为何鼓励大家能用const enum时就用const enum呢?
这是TypeScript为大家特意准备的编译时优化方式,好东西为啥不用呢?编译时优化难道不香吗?
const enum Enum {
A = 1,
B = A * 2,
}
console.log(Enum.A);
将上面的代码编译
enum枚举常量枚举编译效果
var Enum;
(function (Enum) {
Enum[Enum["A"] = 1] = "A";
Enum[Enum["B"] = 2] = "B";
})(Enum || (Enum = {}));
console.log(Enum.A);
const enum 枚举编译效果
console.log(1 /* A */);
外部枚举
所谓外部枚举,即使我们为了在TypeScript开发环境下,更好地使用某些已采用JavaScript编写的库,而被迫为其编写的枚举类型声明。
类型推论
typescript里面有些地方没有指出类型的地方类型推论会帮助提供类型。
最佳通用类型
当我们从几个表达式里推断类型的时候,会使用这些表达式的类型来推断出一个最合适的通用类型。
比如:
let x = [0, 1, null];
里面有两种类型,分别是候选类型number和null,最后会给出一个兼容所有候选类型的类型。比如Array。
由于最终的通用类型取自候选类型,有些时候候选类型共享相同的通用类型,但是却没有一个类型能作为所有类型的候选类型,我们就需要明确的指出类型。(详情请见官网)。
上下文类型
Typescript的类型推论也有可能按照相反的方向进行,叫做上下文归类。按照上下文归类会发生在类型与表达式所处的位置相关的时候。
window.onmousedown = function(mouseEvent) {
console.log(mouseEvent.button); //<- Error
};
检查器会通过左边window.onmousedown推断出mouseEvent的类型,如果函数不是在上下文类型的位置,mouseEvent的类型就要指定为any。
类型兼容性
interface Named {
name: string
}
let x: Named;
let y = {name: string, location: string};
typescript的结构化类型的系统基本规则是,如果x要兼容y,那么y至少要具有与x相同的属性。
这里会检查y是否能赋值给x,编译器会检查x里面的每个属性,看是否能在y中也找到对应的属性。在这个例子中y必须包含的名字是name的string类型成员。
类型兼容的基本规则
具有相同的属性
//基本规则是具有相同的属性
//类似继承,子类型中的属性在父类中都存在,反之则编译失败
//特别说明,TypeScript中类的属性默认值都为undefined
//属性为undefined的不会编译到js文件中去
interface Named {
name: string;
}
class Person {
name: string;
age:number;
}
let p: Named;
//Person没有继承Named
//同样编译通过,运行通过
p = new Person();
p.name='张三丰';
console.info(p);
函数兼容性
形参
//函数兼容性比较
//形参需要包含关系
//形参1是形参2的子类型,参数名字可以不相同
let x=(a:number)=>0;
let y=(b:number,s:string)=>0;
x=y; //编译报错,x参数中没有s参数
y=x;
返回类型
//返回类型,需要被包含关系
//返回类型1,是返回类型2的子类型
let x=()=>({name:'Alice'});
let y=()=>({name:'Alice',location:'Seattle'});
y=x; //编译报错,x中没有返回参数location
x=y;
可选参数和剩余参数
比较函数兼容性的时候,可选参数与必须参数是可互换的。 源类型上有额外的可选参数不是错误,目标类型的可选参数在源类型里没有对应的参数也不是错误。
//上面的例子
let x=(a:number)=>0;
let y=(b:number,s?:string)=>0;
//s不是必选参数,所以x可以赋值y。
x=y;
当一个函数有剩余参数时,它被当做无限个可选参数。
这对于类型系统来说是不稳定的,但从运行时的角度来看,可选参数一般来说是不强制的,因为对于大多数函数来说相当于传递了一些undefinded。
枚举
枚举类型与数字类型兼容
enum Status {
Ready,
Warting
}
enum Color {
Red,
Blue,
Green
}
console.log(Status.Ready == 0);
let status1 = Status.Ready;
console.log(status1);
status1 = 2;//sucess,枚举类型跟数字类型兼容
status1 = Color.Red;//不同枚举类型之间不兼容。
类型
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
私有成员会影响兼容性判断。 当类的实例用来检查兼容时,如果目标类型包含一个私有成员,那么源类型必须包含来自同一个类的这个私有成员。 这允许子类赋值给父类,但是不能赋值给其它有同样类型的类.
泛型
因为TypeScript是结构性的类型系统,类型参数只影响使用其做为类型一部分的结果类型。
interface Empty<T> {
}
let x: Empty<number>;
let y: Empty<string>;
x = y; // okay, y matches structure of x
子类型与赋值
目前为止,我们使用了兼容性,它在语言规范里没有定义。 在TypeScript里,有两种类型的兼容性:子类型与赋值。 它们的不同点在于,赋值扩展了子类型兼容,允许给 any赋值或从any取值和允许数字赋值给枚举类型或枚举类型赋值给数字。
语言里的不同地方分别使用了它们之中的机制。 实际上,类型兼容性是由赋值兼容性来控制的甚至在 implements和extends语句里。
高级类型
交叉类型和联合类型
typescript通过&、|操作符对类型声明进行拓展,用&相连的多个类型是交叉类型。而用|相连的多个类型是联合类型。
两者的区别主要体现在联合类型主要在做类型的合并,而交叉类型主要是在做求同排斥。
就比如说下面的代码,a的类型是接口A和接口B的交叉类型。在都有name属性的情况下,且name的类型一样,符合符合交叉类型的求同。故而a.name可以为string类型。而b的类型是接口A和接口C的交叉类型。name的类型不一样,因为交叉类型的求同排斥
interface A {
name: string;
age: number;
}
interface B {
name: string;
genter: string
}
interface C {
name: number;
genter: string
}
let a: A & B;
let b: A & C;
a.age = 9;
a.genter = "sd";
a.name = '22'
b.name = //error
说白了交叉类型就是数学上的并集,联合类型就是数学上的合集。
类型保护
常用的类型保护有typeof
类型保护,instanceof
类型保护和自定义
类型保护
function BuildURL(param: string | number): any {
if (typeof param === 'string') {
return param.toUpperCase()
}
}
由于使用了typeof
类型保护,所以在if的分支里可以告诉编译器放心的调用string类型的方法,编译器也会给出自动提示