TypeScript语法
基础语法
基础类型和对象类型
TypeScript中基础类型主要包括:number、string、null、undefined、boolean、symbol、void,对象类型包括了{}、[]、class、function等。
//基础类型:number、string、null、undefined、boolean、symbol、void
const count: number = 123;
const str: string = 'lx';
//对象类型:
// 对象
const person: {
name: string,
age: number
} = {
name: 'lx',
age: 18
}
//数组
const numbers: number[] = [1, 2, 3];
// 类
class Person {}
const lx: Person = new Person();
//函数
const getTotal: () => number = () => {
return 123;
}
定义的变量有可能有多种类型如何书写?
const a: number | string = 123;
const arr: (number | string)[] = [1, 'b', 2];
类型注解和类型推断
1、类型注解(type annotation):我们来告诉TS变量是什么类型
2、类型推断(type interface): TS会自动去尝试分析变量的类型
如果TS能自动分析变量类型,我们就不需要写类型注解;如果TS无法分析变量类型,我们就需要雪上类型注解
let countInterface = 1;
const first = 1;
const second = 2;
//如果定义变量写为两行,需要使用类型注解
let count: number;
count = 123;
鼠标移入到变量名上TS会自动分析出变量类型,此时我们就不用去写类型注解
//getTotal中参数需要类型注解,因为不知道调用该方法会传入什么类型的参数
function getTotal(first: number, second: number) {
return first + second
}
//TotalCount为getTotal方法的返回值,现在确定为number类型,所以不用类型注解
const totalCount = getTotal(1, 2)
函数相关类型
1、确定函数返回类型
//函数返回类型为number
function add(x: number, y: number): number {
return x + y
}
// 函数没有返回值
function sayHello(): void {
console.log('hello');
}
// 函数不会执行到最后一行
function errorEmitter() : never {
throw new Error()
console.log(123)
}
2、函数形参解构赋值
// 即使只有一个参数也要用对象的方式
function add({fir, sec}: {fir: number, sec: number}): number {
return fir + sec
}
const total = add({fir: 1, sec: 2});
3、多种定义函数的方式
function fun1(str: string): number {
return parseInt(str, 10)
}
const fun2 = (str: string): number => {
return parseInt(str, 10)
}
// 一般返回类型能够确定就不需要类型注解
const fun2 = (str: string) => {
return parseInt(str, 10)
}
// 下面的类型注解不能省略,因为语法会错误
const fun3: (str: string) => number = (str) => {
return parseInt(str, 10)
}
数组与元组
1、数组
const arr: (number | string)[] = [1, 'b', 2];
const strArr: string[] = ['1', 'b', '2'];
const nullArr: null[] = [null, undefined];
//类型别名 type alias
type Person = {
name: string;
age: number;
}
class Student {
name: string;
age: number
}
// 可以直接new一个实例,只要内部类型一样就可
const objectArr: Person[] = [
new Student(),
{
name: 'lx',
age: 18
}
]
2、元组(tuple):类型和长度都要对应
const studentArr: [string, string, number] = ['lx', 'male', 18];
const studentList: [string, string, number][] = [
['lx', 'male', 18],
['lx1', 'male', 19],
['lx2', 'female', 20]
]
Interface接口
1、官方定义:接口是一系列抽象方法的声明,是一些方法特征的集合,这些方法都应该是抽象的,需要由具体的类去实现,然后第三方就可以通过这组抽象方法调用,让具体的类执行具体的方法。
实际上,就是把一些共有的属性和方法等提取出来。看下面例子:
const getPersonName = (person: {name: string}): string => {
return person.name
}
const serPersonName = (person: {name: string}, name: string) => {
person.name = name
}
getPersonName 和serPersonName 方法中参数person都用到了{name: string}这样的格式校验,所以,现在可以把共有的{name: string}提取出来定义一个接口。
interface Person {
//readonly name: string; 表示name只读
name: string;
age?: number; // ?:表示age可有可无
[propName: string]: any;//表示接口内可以有其他的属性,属性的名字是string类型,值可为任意类型
say(): string;// 属性可以有一个名为say的方法,该方法返回值为string类型
}
const getPersonName = (person: Person): string => {
return person.name
}
const setPersonName = (person: Person, name: string) => {
person.name = name
}
const person = {
name: 'lx',
sex: 'male',
say: () => {return 'say hello'}
}
getPersonName({
name: 'lx',
sex: 'male',
say: () => {
return ''
}
})// 当Person接口中没有sex的定义时,这么写会报错,此时typescript会对其进行强校验
//接口内有[propName: string]: any也不会报错
getPersonName(person)// 当Person接口中没有sex的定义时,缓存一下变量再传进去不会报错
setPersonName(person, 'zr')
2、一个类如果想应用一个接口,可以使用implements
//User类中必须要有name值和say方法;这个类必须要存在接口内存在的方法和属性
class User implements Person {
name = 'lx';
say() {
return 'hello'
}
}
3、接口的继承:
//Teacher 接口继承了Person接口内的所有属性和方法
interface Teacher extends Person {
teach() : string
}
const serPersonName = (person: Teacher, name: string) => {
person.name = name
}
const person = {
name: 'lx',
sex: 'male',
say: () => {return 'say hello'},
teach: () => {
return 'teach'
} // 此时person内必须要有teach方法
}
4、定义函数类型声明
//interface可以定义一个函数类型
interface sayHi {
(word: string): string//函数必须接收一个word的参数,为string类型,返回值类型也是string类型
}
const say: sayHi = (word: string) => {
return word
}
类和类的继承
1、TypeScript 是面向对象的 JavaScript。类描述了创建的对象共同的属性和方法。
class Person {
name = 'lx';
getName() {
return this.name
}
}
const person = new Person();
console.log(person.getName())
2、类的继承和重写
class Person {
name = 'lx';
getName() {
return this.name
}
}
class Teacher extends Person {
getTeacherName() {
return 'teach'
}
// 重写Person类中的getName方法
getName() {
return 'msld' + super.getName() // super代表父类
// super使用场景:子类把父类的方法重写了,但是我还想调用父类的该方法就用super
}
}
const person = new Person();
const teacher = new Teacher()
console.log(teacher.getName()) //maldlx
console.log(teacher.getTeacherName()) // teach
类中的访问类型和构造器
1、访问类型有三种:public、private。protected
public
:类中属性和方法默认是public类型,允许在类的内外被调用
private
:只允许在类内被调用
protected
:只允许在类内及继承的子类中调用
class Person {
name: string;
private age: number;
protected say() {
this.name;
this.age;
return 'hello';
}
}
class Teacher extends Person {
sayHi() {
this.age; // 报错
this.say()
}
}
const person = new Person()
console.log(person.name)
console.log(person.age)// 报错
console.log(person.say())// 报错
2、构造器constructor
每一个类中都有一个constructor方法,在被实例化时会自动执行该方法。
class Person {
//传统写法
// name: string;
// constructor(name:string) {
// this.name = name
// }
//简化写法
constructor(public name: string){}
}
const person = new Person('lx')
console.log(person.name) //lx
如果父类有构造器,子类也要声明构造器,就必须要在子类构造器中调用父类构造器,使用super()代表调用父类的构造器。且还要按照父类构造器的要求把参数传过去。如果父类中没有写constructor,子类中的constructor方法也要调用空的super()
class Person {
constructor(public name: string){}
}
class Teacher extends Person {
constructor(public age: number) {
super('lx');
}
}
const teacher = new Teacher(18)
console.log(teacher.name) //lx
console.log(teacher.age) //18
静态属性、setter和getter
1、静态属性(static):挂载在类上,不是挂载在类的实例上。静态方法内只能访问静态成员,因为静态的内容是随着类的加载而加载的,它是先进入内存的。如果静态方法调用成员变量时,类没有被实例化时则会报错,所以限制静态方法只能使用静态成员
使用static实现一个简单的单例模式:
//单例模式
class Demo {
private static instance: Demo; // 存储new出来的Demo
private constructor(public name: string){}
//static 私有属性,挂载在类上,可直接通过类名调用
static getInstance() {
if(!this.instance) {
this.instance = new Demo('lx');
}
return this.instance;
}
}
const demo1 = Demo.getInstance();
const demo2 = Demo.getInstance(); // 此时demo1和demo2是一模一样的实例
console.log(demo1.name) // lx
console.log(demo2.name) // lx
2、setter和getter
class Person {
//使用private保护私有属性,不让其在类的外部被调用,一般在其前面加下划线表示私有属性
constructor(private _name: string) {}
// 如果想获取私有属性,可以使用get
//现在在外部调用name其实是调用了实际的name值+msld,起到保密作用
get name() {
return this._name + 'msld' // 对name做一些处理后暴露在外部使用
}
// 对设置的name值做了一次处理,也相当于保护了name值
set name(name: string) {
const realName = name.split(' ')[0]
this._name = realName
}
}
const person = new Person('lx');
console.log(person.name) //lxmsld
person.name = 'lx msld'
console.log(person.name)//lxmsld
抽象类
1、抽象类除了不能实例化对象之外,类的其它功能依然存在,成员变量、成员方法和构造方法的访问方式和普通类一样。
2、抽象类不能实例化对象,所以抽象类必须被继承,才能被使用。
3、父类包含了子类集合的常见的方法,但是由于父类本身是抽象的,所以不能使用这些方法。
4、抽象类表示的是一种继承关系,一个类只能继承一个抽象类
//抽象类 里面的属性和方法是它的子类中的一些共有的属性和方法
// 抽象类自己也可以定义自己的属性和方法
abstract class Area{
getName() {
return 'lx'
}
// 定义抽象方法,在子类继承父类时要在子类中写好该抽象方法
abstract getArea(): number
}
class Circle extends Area{
getArea() {
return 1
}
}
class Square extends Area{
getArea() {
return 2
}
}
const circle = new Circle()
console.log(circle.getName())// lx
console.log(circle.getArea())// 1
const square = new Square()
console.log(square.getName())// lx
console.log(square.getArea())// 2
进阶语法
TypeScript中的配置文件
1、tsc --init初始化一个ts项目,会生成一个tsConfig.ts文件(编译配置文件)。如果tsc使用不了,要全局安装TypeScript。
2、使用tsc demo.ts可以编译该文件,这种方式的编译不会执行tsConfig.ts。直接使用tsc编译会编译全部ts文件且执行config文件,但若我只想编译部分ts文件,可以在配置文件中配置 include
、 exclude
或者 files
配置项
{
"include": ["demo.ts"],
"exclude": ["demo1.ts"],
"files": ["demo2.ts"],
"compilerOptions": {
...
}
}
3、compilerOptions中有很多配置项,例举几个:
removeComments: 编译时去除ts文件中的注释
strictNullChecks:不强制进行null校验
rootDir:允许编译的文件路径
outDir:编译后文件存放的路径
incremental:上次编译过的文件就不会再编译,只编译修改过和未编译过的文件
noUnusedLocals:对局部定义未使用的变量做警告
…
更多配置可参考TypeScript官网
联合类型和类型保护
联合类型(Union Types)可以通过(|)将变量设置多种类型,赋值时可以根据设置的类型来赋值
只能赋值指定的类型,如果赋值其它类型就会报错。
类型保护:我们可以通过 typeof
、 instanceof
、 in
和 字面量类型
将代码分割成范围更小的代码块,在这一小块中,变量的类型是确定的。
interface Bird{
fly: boolean;
sing: () => {}
}
interface Dog{
fly: boolean;
bark: () => {}
}
// 联合类型 Bird | Dog 这种方式
function trainAnimal(animal: Bird | Dog) {
if(animal.fly) {
(animal as Bird).sing(); // 断言方式的类型保护
} else {
(animal as Dog).bark();
}
}
function trainAnimalSec(animal: Bird | Dog) {
if('sing' in animal) {// in语法的类型保护
animal.sing();
} else {
animal.bark();
}
}
function add(first: string | number, second: string | number) {
if(typeof first === 'string' || typeof second === 'string') { // typeof语法的类型保护
return `${first}${second}`
} else {
return first + second
}
}
class NumberObj {
count: number;
constructor(){
this.count = 1
}
}
function addSec(first: object | NumberObj, second: object | NumberObj) {
// instanceof语法的类型保护
if(first instanceof NumberObj && second instanceof NumberObj) {
return first.count + second.count
}
return 0
}
Enum枚举类型
枚举使用 enum
关键字来定义
枚举成员会被赋值为从 0 开始递增的数字,可手动赋值,同时也会对枚举值到枚举名进行反向映射:
enum Status {
OFFLINE,
ONLINE = 4,
DELETE
}
console.log(Status.OFFLINE) // 0
console.log(Status.ONLINE) // 4
console.log(Status.DELETE) // 5
console.log(Status[0]) // OFFLINE
手动赋值的枚举项可以不是数字,此时需要使用类型断言来让 tsc 无视类型检查 (编译出的 js 仍然是可用的),只可在最后一项赋值。
enum Status {
OFFLINE,
ONLINE,
DELETE = <any>'a'
}
console.log(Status.DELETE) // a
泛型
泛型(generic):泛指的类型,用<>
包裹
例举某一场景:定义一个方法,传两个参数,此时我想让两个参数的类型必须得一样,但又不确定都是什么类型时可使用泛型
函数中的泛型
//*********函数中的泛型**********
// add方法内两个参数类型都为T,T是不确定的类型,方法调用时才会确定
function Add<T>(first: T, second: T) {
return `${first}${second}`
}
Add<string>('1', '2');
// 泛型内也可以使用多个类型
function Add1<T, P>(first: T, second: P) {
return `${first}${second}`
}
Add1<string, number>('1', 2);
// 方法返回类型也可以使用泛型
function Add2<T, P>(first: T, second: P): T {
return first
}
Add2<number, string>(1, '2');
// Add2<number, string>(1, '2')可以去掉类型写为Add2(1, '2')
// 前者是显示确定类型,后者根据类型推断隐式确定类型
方法的参数类型还可以为一个泛型数组
// ABC[] 和 Array<ABC>等价
function map<ABC>(param: Array<ABC>) {
return param
}
map<string>(['123', 'hello'])
类中的泛型
class DataManage<T>{
constructor(private data: T[]){}
getItem(index: number): T {
return this.data[index];
}
}
const data = new DataManage([1, 2, 3])
console.log(data.getItem(2)) // 3
上面例子中,我想在getItem方法中返回this.data[index].name,因为在构造函数中定义的data是一个泛型数组,现在并不能确定this.data[index]是否有name属性,所以我们可以定义一个接口,然后让T继承该接口。此时我们传的数组参数中的每一项必须要满足有name属性
interface Item{
name: string;
}
class DataManage<T extends Item>{
constructor(private data: T[]){}
getItem(index: number): string {
return this.data[index].name;
}
}
const data = new DataManage([
{
name: 'luo'
},
{
name: 'x'
}])
console.log(data.getItem(0)) // luo
console.log(data.getItem(1)) // x
现在如果我想T的类型只能是string或number类型,不允许是其他类型如何处理?
可使用extends
字段
class DataManage<T extends string | number>{
constructor(private data: T[]){}
getItem(index: number): T{
return this.data[index];
}
}
const data = new DataManage<string>(['123'])
console.log(data.getItem(0)) // 123
泛型中keyof的用法
// 泛型中的keyof
interface Person {
name: string,
age: number,
gender: string
}
class Info {
constructor(private info: Person) {}
getInfo<T extends keyof Person>(key: T): Person[T] {
return this.info[key]
}
}
const info = new Info({
name: 'luo',
age: 18,
gender: 'man'
})
const i = info.getInfo('gender')
console.log(i) //man
keyof Person是对Person这个接口的每一个值做遍历
第一次遍历:
T extends ‘name’ =====> type T = ‘name’
key: ‘name’
返回值类型:Person[‘name’] = string
高级语法
类的装饰器
简单的说,类的装饰器就是对类修饰的一个工具。
1、其本身是一个函数,通过@
符号来使用
2、如果想使用装饰器,需要在tsconfig.josn配置文件中将experimentalDecorators
设置为true
3、装饰器是对类做修饰,而不是对实例做修饰,所以装饰器是在类定义好之后就立即执行。
4、类装饰器接收的参数是构造函数,在装饰器内可以对构造函数进行修改。
function decorator(constructor: any) {
//创建一个getName方法
constructor.prototype.getName = () => {
console.log('luo')
}
}
@decorator
class Test{}
const test = new Test();
(test as any).getName() // luo
可以使用工厂模式来判断是否对这个类做修饰(该方法容易理解但是不太正规)
function decorator(flag: boolean) {
if(flag) {
return function(constructor: any) {
constructor.prototype.getName = () => {
console.log('luo')
}
}
} else {
return function(constructor: any) {}
}
}
@decorator(true) // 通过传参来判断是否需要修饰Test类
class Test{}
const test = new Test();
(test as any).getName() // luo
通过泛型来写正规的装饰器:
对装饰器中的泛型做一些解释:
(...args: any[]) => any
是一个函数,返回值类型是any,参数是一个数组,数组里面值的类型也是any
new (...args: any[]) => any
说明该函数是一个构造函数,T extends new (...args: any[]) => any
表示T是一个类或者说是包含了一个构造函数的东西
function decorator<T extends new (...args: any[]) => any>(constructor: T) {
return class extends constructor{
name = 'msld'; // 无论test实例如何去设置name值,name始终为msld
getName() {
return this.name
}
}
}
@decorator
class Test{
name: string;
constructor(name: string) {
this.name = name
}
}
const test = new Test('luo');
console.log(test); // class_1 { name: 'msld' }
(test as any).getName() // 直接使用test.getName()会报错
在上面所有的例子中,当调用getName方法时我们都不是直接使用test.getName()
去调用。而是用了(test as any).getName()
这样的方式做了处理。因为在Test类中本身没有getName方法,装饰器中新加的方法在实例中是找不到的。那么我们如何处理能让test直接调用getName方法呢?看下面代码:
//同样使用工厂模式 定义一个函数去返回装饰器
function decorator() {
return function <T extends new (...args: any[]) => any>(constructor: T) {
return class extends constructor{
name = 'msld';
getName() {
console.log(this.name)
return this.name
}
}
}
}
// 执行decorator方法 得到一个装饰器 然后再把类当作参数传进去 最后赋值给Test
// 此时Test上就会有装饰器中新加的getName方法了
const Test = decorator()(class {
name: string;
constructor(name: string) {
this.name = name
}
})
const test = new Test('luo');
console.log(test); // class_1 { name: 'msld' }
test.getName() // msld'
方法装饰器
官方定义:方法装饰器声明在一个方法的声明之前(紧靠着方法声明)。 它会被应用到方法的属性描述符上,可以用来监视,修改或者替换方法定义。
简单来说,就是可以对方法做一些修饰。
方法装饰器也是一个函数,接收三个参数:
(1)target:当修饰普通方法时,target对应的是类的prototype
;当修饰静态方法时,target对应的是类的构造函数
(2)propertyKey:方法的名字
(3)descriptor:方法的属性描述符
function getNameDecorator(target: any, propertyKey: string, descriptor: PropertyDescriptor) {
console.log(target, propertyKey, descriptor) // { getName: [Function (anonymous)] } getName {
// value: [Function (anonymous)],
// writable: true,
// enumerable: true,
// configurable: true
// }
descriptor.writable = false
descriptor.value = function() {
return 'decorator';
}
}
class Test{
name: string;
constructor(name: string) {
this.name = name;
}
@getNameDecorator
getName() {
return this.name;
}
}
let test = new Test('luo');
// 配置了descriptor.writable = false 就不可对该方法进行修改
// test.getName = () => {
// return '12345'
// }
// Cannot assign to read only property 'getName' of object '#<Test>'
console.log(test.getName()) // decorator 通过descriptor.value对方法进行了重写
访问器装饰器
访问器装饰器类似于方法装饰器,也为一个函数,且接收的参数和方法装饰器一样。
访问器装饰器声明在一个访问器(get或set
)的声明之前。
function visitDecorator(target: any, propertyKey: string, descriptor: PropertyDescriptor) {
console.log(target, propertyKey, descriptor)
// { getName: [Function (anonymous)], name: [Getter/Setter] } name {
// get: [Function: get],
// set: [Function: set],
// enumerable: true,
// configurable: true
// }
descriptor.writable = false
}
class Test{
_name: string;
constructor(name: string) {
this._name = name;
}
getName() {
return this._name;
}
@visitDecorator
get name() {
return this._name
}
set name(name: string) {
this._name = name
}
}
let test = new Test('lx');
// 配置了descriptor.writable = false 不能修改name
test.name = 'lll'
// Invalid property descriptor. Cannot both specify accessors and a value or writable attribute
console.log(test.name)
注意:TypeScript不允许同时装饰一个成员的get
和set
访问器,因为在装饰器应用于一个属性描述符(descriptor)时,它联合了get和set访问器,而不是分开声明的。
属性装饰器
属性装饰器同样是一个函数,使用@
符号放在定义的属性之前,与上面的装饰器相比,有以下几点区别:
(1)函数参数只有两个:target
和propertyKey
,不接受descriptor
(2)如果想设置descriptor,需要在函数内部定义一个descriptor来实现
function nameDecorator(target: any, propertyKey: string): any {
const descriptor: PropertyDescriptor = {
writable: false
}
return descriptor
}
class Test{
@nameDecorator
name = 'luo';
}
let test = new Test();
test.name = 'msld' //TypeError: Cannot assign to read only property 'name' of object '#<Test>'
(3)不能在函数内直接修改属性的值,因为修改的是原型上的属性值,而不是实例上的属性值
function nameDecorator(target: any, propertyKey: string): any {
target[propertyKey] = 'msld' // 这里修改的并不是实例上的name,而是原型上的name
}
class Test{
@nameDecorator
name = 'luo';
}
let test = new Test();
console.log(test.name) // luo 这里不会输出装饰器中设置的name值msld
console.log((test as any).__proto__.name) // msld
参数装饰器
参数装饰器同样是一个函数,使用@
符号放在定义的方法参数之前。
参数装饰器接收三个参数:target
、propertyKey
(不是参数名,而是方法名)、parameterIndex
(第几个参数)。
注意:参数装饰器只能用来监视一个方法的参数是否被传入。
function paramDecorator(target: any, propertyKey: string, paramIndex: number){
console.log(target, propertyKey, paramIndex) //{ getInfo: [Function (anonymous)] } getInfo 0
}
class Test{
getInfo(@paramDecorator name: string, age: number) {
console.log(name, age)
}
}
let test = new Test();
test.getInfo('lx', 18) //lx 18