TypeScript详细讲解

介绍

typescript是一门由微软公司发布的开源编程语言,目前主流的前端框架vue、react、angular都支持使用typescript来进行开发,typescript被认为是JavaScript的超集,主要提供了类型系统和对es6的支持

typescript的优势

  • 提供了类型系统,使得代码比如函数一眼就可以看出如何使用,便于代码的维护和语义化。有了类型系统使得很多问题在编译的时候就报错了,这样比在运行时就出错更加方便,更早的暴露已知也能提高开发效率

  • typescript是JavaScript的超级,js文件可以直接名称成ts。typescript即使不显式的定义类型也能进行类型推论。typescript代码即使编译报错也会生成JavaScript代码

  • 兼容第三方库,即使第三方库不是使用ts写的,也可以编写单独的类型文件供 TypeScript 读取,当然大部分第三方库也是提供了ts的版本

  • ts支持了es6和部分未来即将进入es7、es8的部分规范

  • ts学习成本偏高,需要有一定的OOP基础,前端对接口、泛型、类、枚举类型有一定的难度

javascript数据类型

  • 在JavaScript中数据类型分为基本数据类型和引用数据类型
  • 基本数据类型String Boolean Number null undefined symbol
  • 引用数据类型Object Function Date Error RegExp

typescript中数据类型

  • 变量的类型系统就是控制变量类型的随意变化,变量一旦定义为某种类型就不能被修改,因此ts是强类型语言,而JavaScript中变量的类型是可以随意变化的,归类为弱类型语言
  • boolean 基本数据类型-布尔值
let isHandsome: boolean = false
  • typescript中也有布尔值对象,注意这个是布尔值对象,其他string、number类似
let isHandsome: Boolean = new Boolean(1)
  • number 原始数据类型-数字,二进制和八进制在编译时会被编译成十进制
let count: number = 100
// 二进制
let computer: number = 0b10101
// 八进制
let age: number = 0o677
// 十六进制
let salary: number = 0x00A
  • string 原始数据类型-字符串,typescript中也支持模板字符串
let str: string = 'you are handsome'
 
let sentance: string = `I know ${str}`
  • void 空值,在JavaScript中没有空值的概念,在ts中void可以作为函数的返回值,当然也可以定义一个空值,但是并没有实际的意义,因为你只能给他赋值undefined和null
function test (): void {
  let params: void = undefined
  let params2: void = null
  console.log('我是没有返回值的函数')
}
  • null和undefined是所有类型的子类型,可以赋值给任意类型,当然也可以自己定义类型。与void的区别是:void类型的变量不能随意赋值给其他类型的变量
let u: undefined = undefined
let n: null = null
let str2: string = undefined
let str3: string = null
let num: number = undefined
let num2: number = null
  • any 任意值,表示变量可以赋值为任意类型,当然可以是对象类型,也可以调用方法
let hobby: any = '打麻将'
console.log(hobby.name)
hobby = 100
hobby = false
hobby = undefined
  • 未声明类型的变量,在ts中一个变量如果没有声明类型就会默认为any类型
let person
person = 'Mike'
person = {
  say () {
    console.log('hehe')
  }
}
person.say()
  • 联合类型(union types)表示取值可以是声明的某一种,联合类型注意:当一个变量是联合类型时,并且我们不能确定他是什么类型时只能访问变量联合类型都有的属性和方法,否则会报错
let myFavorite: string | number
myFavorite = '我可以是字符串类型和数字类型'
// 类型推论系统会推断出myFavorite是字符串类型,可以访问length属性
console.log(myFavorite.length)
myFavorite = 1000
// 类型推论系统会推断出myFavorite是数字类型,不可以访问length属性
myFavorite.length
// 数字类型可以使用
myFavorite.toFixed(2)
 
function testUnion (something: string | number): void {
  // 数字类型是没有length属性,这样就会报错
  console.log(something.length)
  // 数字类型和string类型都有toString方法
  // 正常访问
  console.log(something.toString())
 
}
  • typescript中的对象类型–接口

在面向对象编程中,接口是对行为的抽象,而具体的行动需要由类(class)去实现(implements),在typescript中接口不仅能对类的行为进行抽象,还可以对“对象的形状”进行描述

使用接口类型定义的变量必须保持接口要求的“形状”,不能多也不能少,接口可以要求可选参数、任意添加参数

处于某些设计,我们需要对某些参数只允许进行读取而不能修改,可以添加readonly来对参数进行修饰,使用readonly修饰的参数只能在第一次变量被对象赋值时,后期如果对该参数进行赋值就会报错,因为该参数只可读

// 接口建议是首字母大写,大驼峰
interface Person {
  name: string,
  age: number
}
 
// 定义了接口Person又定义了一个变量tom而且tom类型是接口Person这样tom的形状就被约束了必须是Person
// 变量tom的形状必须保持与接口Person一样,不能多也不能少,不然会报错,当然也是可以定义可选参数的
let tom: Person = {
  name: 'Tom',
  age: 18
}
 
// 当然ts也提供了可选参数,允许进行变量与接口进行不完全匹配
interface Student {
  name: string,
  age: number,
  class?: string
}
// 可选参数表示可有可无,但是接口中未定义的参数依然是不允许添加的
let st1: Student = {
  name: 'mike',
  age: 19,
  class: 'class one'
}
let st2: Student = {
  name: 'mike',
  age: 19
}
 
// 如果希望一个接口可以添加任意属性并且是任意类型,可以使用以下定义
interface Text {
  name: string,
  age: number,
  [propName: string]: any
}
 
// 注意,如果允许添加任意属性,那么接口中的参数必须是propName所规定的类型
// age是number类型,但是允许添加任意类型是string类型,这样就会报错
interface Texts {
  name: string,
  age: number,
  [propName: string]: string
}
 
// 添加readonly对参数进行修饰,这样的参数就只能被读取,而不能被修改
interface one {
  readonly id: number,
  name: string,
  age: number,
  [propName: string]: string
}
  • 数组,ts中数组定义方法多,使用灵活

数组一旦定义为某个,数组中成员必须都是该类型,否则编译时报错。当然也有any类型的数组

let arr: number[] = [1, 2, 3]
// 类型推断系统会推断出arr是一个number数组类型,非法,不通过编译
arr.push('4')
 
// 非法,不通过编译
let arr2: number[] = [1, '2', 3]
 
// 联合类型,合法通过编译
let arr3: (number | string)[] = [1, '2', 3]
 
// 数组的any类型
let arr5: any = [1, 'hello', { age: 10 }]
  • 数组泛型-也是定义数组的一种方法

// 泛型,即参数化类型,在使用的时候传入需要的类型,如果类型对不上在编译时就会报错
let arr4: Array<number> = [1, 2, 3]
  • 接口描述数组

// 表示索引必须是number类型,值也必须是number类型
interface NumberArray {
  [index: number]: number
}
  • 类数组-并不是数组类型

比如函数的参数列表arguments,常见的类数组都有自己(内置对象)的接口定义,比如arguments的接口定义就是IArguments

function test (): void {
  console.log(arguments)
  let args: IArguments = arguments
}
  • 函数类型

  1. 函数是一等公民
  2. 常见函数定义的两种方式:函数声明和函数表达式
  3. 函数有输入和输出,在ts中可以对函数的输入和输出进行类型约束
  4. 在typescript中函数形参和实参必须是一一对应的,不能多也不能少,不然无法通过编译,当然也是可以标记可选类型的
  5. 可以使用接口定义函数形状
  6. 函数形参可以配置默认值,如果形参配置了默认值,默认就是可选参数
  7. es6中的扩展运算符…
  8. 方法(函数)的重载,一个函数接受不同的参数做出不同的行动,函数重载需要注意的是,typescript会从函数最早的定义开始识别匹配,所以多个相同的函数如果有包含关系就需要把精确的定义先写在前面,最后写实现,函数的重载目的就是为了更好的语义话函数定义
// javascript中的函数有输入有输出
function test (x,y) {
  return x + y
}
 
// typescript中的函数可以对输入和输出进行约束
function sum(x: number, y: number): number {
  return x + y
}
 
// typescript中的函数设置可选参数,必填参数放前面,可选放后面
function sum(x: number, y?: number): number {
  return x + y
}
 
// 函数表达式,但是这个通过赋值操作的类型推断的
let mySum = function (x: number, y: number): number {
  return x + y
}
 
// 函数表达式手动添加类型
// 注意 => 不是es6中箭头函数,这个符号右边代表的是输出类型,左边代表的是输入类型
let mySum2: (x: number, y:number) => number = function (x: number, y:number): number {
  return x + y
}
 
// 使用接口定义函数的形状
interface Search {
  (source: string, subString: string): boolean
}
let my: Search
my = function (source: string, subString: string) {
  return source.search(subString) !== -1
}
 
// 形参默认值
function getFullName (firstName: string, lastName: string = 'Mike'): string {
  return firstName + lastName
}
let result = getFullName('Jackson')
 
// 数组的扩展运算符
function myPush(array: any[], ...items: any[]): void {
  items.forEach(item => {
    array.push(item)
  })
}
myPush([], 1, 2, '3')
 
// 需求,根据输入的数字的类型和字符串进行反序
// 如果不使用重载,使用联合类型
function myReverse (x: string | number): string | number {
  if (typeof x === 'number') {
    return Number(x.toString().split('').reverse().join(''))
  } else if (typeof x === 'string') {
    return x.split('').reverse().join('')
  }
}
// 使用重载
function myReverse2 (x: string): string
function myReverse2 (x: number): number
function myReverse2 (x: string | number): string | number {
  if (typeof x === 'number') {
    return Number(x.toString().split('').reverse().join(''))
  } else if (typeof x === 'string') {
    return x.split('').reverse().join('')
  }
}

typescript的类型推论

typescript类型推论系统会在变量的第一次声明和赋值的时候推断出这个变量的类型,类型推论使用场景:变量定义后但是没有指定类型,类型推论系统就会根据赋予变量的值进行推断。还有一种场景就是联合类型的时候哦哦慢慢买牛奶,

let result = 'what the fuck'
// typescript的类型推论系统已经推断出result的类型是string再赋值number就会报错
result = 100

typescript的类型断言-手动指定变量类型

在联合类型中,我们并不知道变量一定是什么类型,但是有时候我们又需要提前调用变量的某个属性或者方法,这个时候我们就可以断言该变量是某种类型,但是规矩是,断言的类型必须是联合类型中的一种,如果最终传入的变量不是断言的类型,访问依然可以进行,但是会得到undefined

类型断言支持2中语法,<要断言的类型>或者as

// 类型断言
function test (variable: string | number): void {
  // 现在就想使用字符串的length属性
  console.log((<string>variable).length)
  console.log((variable as number).toFixed(2))
}
// 得到长度5但是字符串没有toFixed方法会报错,编译可以通过
test('hello')
// number类型没有长度,得到undefined,编译可以通过
test(123)

声明文件

在使用第三方库,一些全局关键字在typescript中我们并不知道他是什么,比如jQuery中的$和jQuery

例如$("#id")或者jQuery("#id") 在typescript直接使用是会报错的,这个时候就需要用到声明文件

通常我们会把类型声明单独放到一个文件中,这就是声明文件,还会约定声明文件以.d.ts结尾,然后需要用到声明的文件的最开头使用///进行引用声明文件

declare var jQuery: (selector: string) => any
 
// 引用,在需要声明文件的文件最开始使用 /// 进行引用声明文件
 
/// <reference path="./jQuery.d.ts" />

第三方声明文件

一般不需要自己来声明,网上都有别人声明好了的,typescript还推荐开发者使用@types来管理第三方的库

比如安装jQuery:npm install @types/jquery --save-dev

内置对象

在JavaScript中有很对内置对象如Error、Date、RegExp、Boolean、Number 等等

在typescript中我们可以根据这些内置对象创建变量

内置对象的定义文件都在typescript的核心库定义文件中

let bool: Boolean = new Boolean(1)
let err: Error = new Error('Error occurred')
let date: Date = new Date()
let reg: RegExp = /[a-z]/

DOM和BOM的内置对象

有Document、HTMLElement、Event、NodeList等

内置对象的定义文件都在typescript的核心库定义文件中

let body: HTMLElement = document.body
let allDiv: NodeList = document.querySelectorAll('div')
document.addEventListener('click', function (e: MouseEvent) {
  // Do
})

typescript 核心库的定义文件

typescript 核心库的定义文件定义了浏览器环境所有需要的类型,并且预置在typescript中,我们在使用一些常用的方法时,其实typescript已经做了很多的类型判断工作,比如pow(10,‘2’)是会报错的

我们在ts中使用的很多方法和事件,ts在背后都做很多的类型推断工作,这些定义文件都在typescript的核心库文件中

Node

在typescript中没有内置Node,如果想要使用ts写Node那就需要引入第三方声明文件

npm install @types/node --save-dev

typescript进阶

  • 类型别名

类型别名,作用就是给类型取一个新名字,类型别名多用于联合类型时简化

// 直接赋值定义
type Name = string
 
// 使用函数表达式定义
type NewName = () => string
 
// 别名之间进行赋值
type NewNames = Name | NewName
  • 字符串字面量类型

字符串字面量类型,作用是用来约束某个变量取值只能是规定的字符串中某一个

// 字符串字面量类型
type EventNames = 'click' | 'scroll' | 'mousemove'
 
function handleEvent (ele: Element, event: EventNames) {
  // do something
}
 
// 正常运行
hansleEvent(document.getElementById('test'), 'click')
 
// dbclick不是EventNames定义的类型,编译时报错
hansleEvent(document.getElementById('test'), 'dbclick')
 
 
// 注意,类型别名和字符串字面量类型都是使用type关键字来定义
  • 元组

数组合并相同类型的对象,而元组合并了不同类型的对象,在ts中除了any类型的数组,其他类型的数组都只能存一种类型

元组定义时有几个类型就说明了长度,赋值时不能多也不能少,访问可以跟数组一样的访问方式

// 元组,类型个数指定长度,一一对应,不多不少
let personMessage: [string, number] = ['lisi', 19]
 
// 访问元素,跟数组一样访问
personMessage[0]
 
// 可以通过push方法添加越界元素,但是只能添加元组规定的类型,其他类型会报编译错误
personMessage.push('lisii')
  • 枚举

Enum枚举类型,作用是将取值限制在一定范围内,比如一周只有7天

使用关键字enum声明

enum Days { Sun, Mon, Tue, Wed, Thu, Fir, Sat }
 
// 枚举成员会被从0开始依次递增赋值
Days['Sun'] === 0 // 为true
 
// 同时枚举值到枚举名会被进行反向映射
Days[0] === 'Sun' // 为true
 
// 事实上枚举会被编译为
var Days;
(function (Days) {
    Days[Days["Sun"] = 0] = "Sun";
    Days[Days["Mon"] = 1] = "Mon";
    Days[Days["Tue"] = 2] = "Tue";
    Days[Days["Wed"] = 3] = "Wed";
    Days[Days["Thu"] = 4] = "Thu";
    Days[Days["Fri"] = 5] = "Fri";
    Days[Days["Sat"] = 6] = "Sat";
})(Days || (Days = {}))
 
// 枚举类型也可以手动赋值,未赋值的枚举项会接着上一个枚举项递增
enum Eunms { red = 2, blue, yellow }
 
// 如果手动赋值跟自动赋值的枚举项值重复了,typescript并不会察觉到这个变化,会正常进行,所以我们在手动赋值的时候就要避免这种情况出现
enum Eunmss { red = 2, blue = 1, yellow, green, pink }
 
Eunmss[2] === 'red' // true
Eunmss[2] === 'yellow' // true
 
// 当然手动赋值可以不是数字,可以是字符串,也可以是小数负数,但是递增步长还是1
// 使用字符串赋值时需要类型断言来告诉tsc不进行类型检查
enum Enumss {c, b, a = <any>'s' }
 
// 使用小数
enum EnumsNum {
  a = 1,
  b = 1.3,
  c
}
 
// 枚举项分为常数项和计算所得项,以上都是常数项
// 实例计算所得项,'hello'.length就是一个计算所得项
enum Cal {
  a = 2,
  b = 1,
  c,
  d = 'hello'.length
}
 
// 但是如果计算所得项后面还有枚举项就会导致后面的枚举项得不到初始值而报错
enum Cals {
  a = 'hello'.length,
  // 编译时就会报错
  b
}
 
 
// 常数项总结
// 不具有初始化 函数并且之前的枚举成员都是常数,这种情况下枚举成员的值是上一个枚举成员的值加1,但是第一个成员例外,默认值为0
// 引用之前定义的常数枚举成员
// 带括号的常数枚举表达式
// + - ~ 一元运算符应用于常数枚举表达式
// + - * % / | & ^ 二元运算符,常数枚举表达式作为其一个操作对象,求值后为NaN或者Infinity则会在编译时报错
// 其他均为计算所得项
 
 
 
// 常数枚举,使用const enum 来定义,常数枚举跟普通枚举的区别就是,它在编译阶段就会被删除,并且不能包含计算成员
const enum ConstEnum {
  Up,
  Down,
  Left,
  Right
}
let directions = [ConstEnum.Up, ConstEnum.Down, ConstEnum.Left, ConstEnum.Right]
// 编译结果 let directions = [0, 1, 2 ,3]
 
 
// 外部枚举, 使用declare enum 来定义,外部枚举和声明语句一样,常出现在声明文件中
// 使用declare定义的类型只会用于编译时的检查,编译结果中会被删除
declare enum ConstEnums {
  Up,
  Down,
  Left,
  Right
}
let directionss = [ConstEnum.Up, ConstEnum.Down, ConstEnum.Left, ConstEnum.Right]
// 编译结果 let directionss = [ConstEnum.Up, ConstEnum.Down, ConstEnum.Left, ConstEnum.Right]
 
 
// 同时使用声明和常数枚举也是可以的
declare const enum DC {
  Up,
  Down,
  Left,
  Right
}
let directionsss = [DC.Up, DC.Down, DC.Left, DC.Right]
// 编译结果let directionsss = [0, 1, 2 ,3]
 
// typescript的枚举类型来源于C#

传统的JavaScript中类是通过构造函数来实现的,es6中出现了类和继承,底层实现还是构造函数

typescript中的类型

类,定义了一件事物的抽象特点,包含它的属性和方法

对象,类的实例,通过 new 生成

面向对象(OOP)3大特性,继承、封装、多态

封装,将数据操作细节隐藏起来,只对外提供操作接口。外界调用也不需要知道细节,直接使用提供的接口来访问该对象,外部也不能随便修改更改对象内的数据

继承,子类继承父类,子类除了拥有了父类的所有特性,还有自己的特性

多态,由继承产生了相关的不同的类,对同一个方法有不同的响应。比如Cat和Dog都继承自Animal,但是都实现了自己的叫的方法。此时针对某一实例,我们不需要了解他是Cat还是Dog就可以直接调用叫这个方法,程序会自动判断是喵还是汪

存取器(getter和setter),用于改变属性的读取和赋值行为

修饰符,修饰符是一些关键字,用于限定成员或类型的特性,比如public表示公有属性和方法

抽象类,抽象只能供其他类继承,不能实例化对象,抽象类中的抽象方法必须在子类中实现

接口,不同类之间的公有属性或方法可以抽象成一个接口,接口可以被类实现,一个类只能继承自另一个类,但是可以实现多个接口

  • ES6中的类

使用class定义,使用constructor定义构造函数,使用 new 生成对象实例,会自动调用构造函数

// es6中类的定义
class Person {
  constructor (name) {
    this.name = name
  }
  sayHello () {
    console.log(`my name is ${this.name}`)
  }
}
 
let person = new Person('peter')
person.sayHello()
 
// es6中类的继承
class Student extends Person {
  constructor (name, classes) {
    super(name)
    this.classes = classes
  }
  saySelf () {
    console.log(`I am ${this.name} from ${this.classes}`)
  }
}
 
let student = new Student('jack', '2班')
student.saySelf()
student.sayHello()
 
// es6类中的存取器,使用getter和setter改变属性的赋值和读取行为
class Animal {
  constructor (name) {
    this.name = name
  }
  get name () {
    return 'Tom'
  }
  set name (value) {
    console.log(`setter: ${value}`)
  }
}
let mouse = new Animal('jerry')
 
 
// es6中的静态方法,使用static修饰,它们不需要被实例化,也不会被实例化,直接使用类就可以调用
class Car {
  constructor (color) {
    this.color = color
  }
  static sayStaticSelf () {
    console.log('I am a Car')
  }
  saySelf () {
    console.log(`I am a ${this.color} Car`)
  }
}
let redCar = new Car('red')
redCar.saySelf()
// sayStaticSelf没有被实例化,调用会报错,not a function
// redCar.sayStaticSelf()
 
 
// es7中类新增的内容,在es6中实例的属性只能通过构造函数的this.xxx = xxx 来实现,es7提案中直接可以在类里面定义
class One {
  name = 'lily'
  // 属性也可以加static修饰成静态属性,只能使用类之间访问
  constructor () {
    // ...
  }
}
let onePerson = new One()
// node暂不支持执行
console.log(onePerson.name)
  • typescript中的类

typescript中可以使用的3个修饰符

  1. public,修饰的属性和方法是公有的,可以在任何地方访问到,默认没有使用修饰符的方法和属性都是public
  2. private,修饰的属性和方法是私有的,不能在声明它的类的外部访问,只能在类里面被访问
  3. protected,修饰的方法和属性是受保护的,它可以在子类中被访问,也可以在自己类中访问
// public 修饰符,外部可读可取
class PubAnimal {
  public name;
  constructor (name) {
    this.name = name
  }
}
let a = new PubAnimal('Cat')
console.log(a.name) // Cat
a.name = 'Dog' // Dog
 
// private 修饰符,外部无法存取,会报错
class PriAnimal {
  private name;
  constructor (name) {
    this.name = name
  }
}
let b = new PriAnimal('Cat')
console.log(b.name) // 报错
b.name = 'Dog' // 报错
 
// protected 修饰符,子类是可以访问的
class ProAnimal {
  protected name;
  constructor (name) {
    this.name = name
  }
}
class ProExAnimal extends ProAnimal {
  constructor (name) {
    super(name)
    // 子类可以访问
    console.log(this.name)
  }
}
 
// 抽象类,使用abstract修饰,抽象类不能被实例化,只能被继承,抽象类中抽象方法必须在子类实现
// 有抽象方法一定是抽象类,但是抽象类不一定有抽象方法
abstract class AbsAnimal {
  public name;
  public constructor (name) {
    this.name = name
  }
  // 定义抽象方法必须在子类中实现
  public abstract sayHello()
}
 
class AbsExAnimal extends AbsAnimal {
  // 可以拥有自己的方法和属性
  public eat () {
    console.log('I am eat')
  }
  // 子类需要实现父类的抽象方法
  public sayHello () {
    console.log('hello world')
  }
}
 
// 可以给类加上类型控制
class TypeAnimal {
  name: string
  constructor (name: string) {
    this.name = name
  }
  sayHello (): string {
    return this.name
  }
}
  • 类与接口

实现(implements)是面向对象的一个重要概念,一般来讲,一个类只能继承自另一个类,有时候不同的类之间有一些共有的特性,就可以把这些共有的特性提取成接口,然后使用implements来实现,目的在于提高面向对象的灵活性

现实举例:门是一个类,防盗门是门的一个子类,防盗门有一个报警器功能,我们可以简单的给防盗门加一个报警方法。这时候如果有类,车,也有报警功能,我们就可以考虑把报警功能提取出来作为一个接口然后防盗门类和车类都去实现它

interface Alarm {
  // 描述报警alert方法,类去具体实现
  alert()
}
 
interface Light {
  // 开灯方法
  openLight()
  // 关灯方法
  closeLight()
}
 
class Door {}
 
class SecurityDoor extends Door implements Alarm {
  alert () {
    console.log('warning ! ! !')
  }
}
 
// 一个类可以实现多个接口,使用逗号隔开
class AlCar implements Alarm, Light {
  alert () {
    console.log('ba ba ba ba')
  }
  openLight () {
    console.log('open light')
  }
  closeLight () {
    console.log('close light')
  }
}
 
// 接口可以继承接口,其实就是两个接口合并,使用关键字extends
interface LightAlarm extends Alarm {
  openLight()
  closeLight()
}
 
// 接口也可以继承类
class Point {
  x: number
  y: number
}
 
interface Point3d extends Point {
  y: number
}
 
let point3d = {
  x: 1,
  y: 2,
  z: 3
}
 
// 混合类型,使用接口的方式来定义一个函数需要符合的形状
interface SearchFunc {
  (source: string, subString: string): boolean
}
let mySearch: SearchFunc
mySearch = function (source: string, subString: string) {
  return false
}
 
// 有时候一个函数还可以拥有自己的属性和方法,使用接口描述函数形状如下
interface Counter {
  (start: number): string
  interval: number
  reset(): void
}
function getCounter(): Counter {
  let counter = <Counter>function (start: number) {}
  counter.interval = 11
  counter.reset = function () {}
  return counter
}
let  c = getCounter()
c(10)
c.reset()
c.interval = 5
  • 泛型,即参数化类型

泛型是指在定义函数、接口、类的时候,不事先去指定具体的类型,而是在使用的时候再去指定类型的一种特性

// 泛型的引入,首先我们设计一个函数createArray,他可以创建指定长度的数组,同时将每一项都填冲一个默认值
function createArray (length: number, value: any): Array<any> {
  let result: = []
  for (let i = 0; i < length; i++) {
    result[i] = value
  }
  return result
}
createArray(3, 'x')
// 以上使用了泛型来定义数组的返回值的类型,以上编译也不会失败,但是有一个缺陷就是他没有准确的定义返回值的类型
// Array<any> 允许数组的每一项为任何类型,但是我们的预期是它应该是我们输入提供的value的类型,这个时候就可以使用泛型来解决
function createArrayT <T>(length: number, value: T): Array<T> {
  let result: T[] = []
  for (let i = 0; i < length; i++) {
    result[i] = value
  }
  return result
}
createArrayT<string>(3, 'x')
createArrayT(3, 'x')
// 在函数名后面添加<T>,其中T用来指代任意输入的类型,然后在后面的value: T 和Array<T> 就可以直接使用了
// 在进行方法调用的时候可以手动指定它的具体类型为string,当然也可以不指定,让类型推断自己推算出来
 
 
// 多个类型参数,定义泛型的时候,使用逗号隔开,可以一次性定义多个类型参数
// 定义了一个swap函数,用来交换输入的元组
function swap<T, U>(tuple: [T, U]): [U, T] {
  return [tuple[1], tuple[0]]
}
 
 
// 泛型约束,在函数内部使用泛型的时候,由于事先不知道它是哪种类型,所以不能随便操作它的属性和方法,会报错
function Test <T>(arg: T): T {
  // arg类型位置不能直接使用,会报错,但是我们可以使用泛型约束,使用接口来描述arg
  console.log(arg.length)
  return arg
}
 
// 使用接口描述arg
interface Arg {
  // arg必须有length属性
  length: number
}
// 让T去继承接口Arg就可以约束传入的类型必须是符合Arg描述的样子
function Tests <T extends Arg>(arg: T): T {
  // 使用泛型约束,使用接口来描述arg,编译通过
  console.log(arg.length)
  return arg
}
// 调用方法Tests时传入的参数也要符合T的描述,必须传入一个带length属性的参数
Tests(7) // 数字没有length
Tests('hello') // 字符串有length
 
 
 
// 多个类型参数之间互相约束
function copyFields<T extends U, U>(target: T, source: U): T {
  for (let key in source) {
    target[key] = (<T>source)[key]
  }
  return target
}
let x = {a: 1, b: 2, c: 3, d: 4}
copyFields(x, { b: 10, d: 20 })
// T继承自U,这样就保证了U上不会出现T中不存在的字段,也保证了T中不会出现U中不存在的字段
 
 
// 泛型接口,使用接口可以描述一个函数的输入输出
interface IFunc {
  (name: string, age: number): boolean
}
let myIFunc: IFunc
myIFunc = function (name: string, age: number) {
  return false
}
// 可以使用含有泛型的接口来定义
interface IFuncc {
  <T>(name: string, value: T): Array<T>
}
let myIFuncc: IFuncc
myIFuncc = function <T>(name: string, value: T) {
  let result: T[] = []
  for (let i = 0; i < 3; i++) {
    result[i] = value
  }
  return result
}
 
// 在接口中使用泛型,可以把泛型参数提前到接口名上
interface IFunccc<T> {
  (name: string, value: T): Array<T>
}
// 在某处使用接口时,需要指定泛型的类型
 
 
// 泛型类。泛型也可在类中使用
class GenericNum<T> {
  num: T
  add: (x: T, y: T) => T
}
 
// 泛型参数在typescript2.3后可以给默认值
function defaultValue<T = string>(input: T): T {
  return input
}
  • 声明合并

如果定义了两个相同名字的函数、接口或者类,那么他们会合并成一个类型

// 函数合并
function reverse(x: number): number;
function reverse(x: string): string;
function reverse(x: number | string): number | string {
  if (typeof x === 'number') {
    return x + 1
  } else if (typeof x === 'string') {
    return x + 1
  }
}
 
 
// 接口合并
interface Alarm {
  price: number
}
interface Alarm {
  weight: number
}
// 合并后结果
interface Alarm {
  price: number;
  weight: number;
}
// 注意如果两个同名的接口中属性名相同,但是类型不同,这种情况会报错,类型相同正常合并
// 注意如果两个同名的接口中方法名相同,跟正常的函数合并一样
// 两个同名类合并跟 接口合并规则一致

猜你喜欢

转载自blog.csdn.net/weixin_45679977/article/details/104953511