Skip to content

TypeScript

JavaScript的超集,拓展了JavaScript,并添加了类型 可以在任何支持Javascript的平台中执行,不能被js解析器直接执行, 需要编译转换(类比less, scss)

  • 静态类型, 编译阶段就进行类型检查π
  • 弱类型, 不会修改JavaScript运行时的特性

入门教程: https://ts.xcatliu.com

深入理解 TypeScript: https://jkchao.github.io/typescript-book-chinese/faqs/tsconfig-behavior.html

非官方中文文档: https://zhongsp.gitbooks.io/typescript-handbook/content/

阮一峰: https://wangdoc.com/typescript/

直接运行

sh
npm install -g ts-node
ts-node xxx.ts

ts-node不会监听文件变化, 可以使用ts-node-dev

tsc 根据config转化ts文件到js

tsx 基于 esbuild 打造的执行TypeScript 文件的命令行工具。它比 ts-node 效率更高,使用起也更加简单。

编译

  1. 下载并安装node.js
  2. npm i -g typescript全局安装ts编译器
  • 自动编译

    • 监控单个文件 tsc xxx.ts -w*

    • 监控整个项目

      1. 要有相关配置文件tsconfig.json, 可以用tsc -- init生成
      2. 配置文件里只要一对大括号, 就能生效
      3. tsc
  • 配置选项

    • include: 定义希望被编译文件所在的目录

      json
      "include": [
          "./src/**/*"
      ]
      // 两个星号表示任意目录, 一个星号表示任意文件
    • exclude: 排除目录

    • extends: 继承的配置文件

    • files: 指定被编译的文件列表, 只有需要编译的文件少时才会用到

    • ==compilerOptions==: 编译器的选项 (技巧, 写个错的, 报错信息会显示哪些可选项)

      • "target": "es2015" es版本

      • "module": "es2015" 使用的模块化规范

      • // "lib":[] 项目使用的库, 一般不改

      • "outDir": "./dist" 编译后输出的目录

      • "outFile": "./dist/app.js" 将全局作用域的代码合并到一个文件

      • "allowJs": false 是否对js文件进行编译, 默认false

      • "checkJs": false 检查js是否符合规范 , 默认是false

      • "removeComments": false 是否移除注释

      • "noEmit": false 只检查语法, 不生成编译后的文件

      • "noEmitOnError": false 有错误时不生成编译后的文件

      • "strict": true 所有的严格检查开关, 建议打开

      • "alwaysStrict": false 编译后的文件是否使用js严格模式

        当使用import, export时, 自动进入严格模式 , js中不会再生成"use strict"

      • "noImplicitAny": true 不允许隐式any类型

      • "noImpliciThis": true 不允许不明确类型的this,

    ts
    function fn1(){
        console.log(this)	//这里的this是不明确的, 不能确定谁会调用它
    }
    
    // 如果只想让能明确的对象来调用它, 怎么解决
    function fn1(this: window){
        console.log(this)
    }
    • "strictNullChecks": true 严格的检查空值
    ts
    let box = documnet.getElementById('box')
    box.addEventListener(type: 'click', listener: function(){
        alert('hello')
    })
    // 这里的box可能拿不到, 后面的绑定事件就会出错
    // 配置了严格检查空值, 这里就会有提示
    
    // 怎么解决
    let box = documnet.getElementById('box')
    if(box !== null){
      	  box.addEventListener(type: 'click', listener: function(){
      	  alert('hello')
        })
    }
    
    // 另一种写法
    box?.addEventListener(type: 'click', listener: function(){
        alert('hello')
    })

类型声明

ts最明显的特征, 就是为js加上类型声明

  1. 声明并定义类型
  2. 赋值时自动定义类型
  3. 函数的参数和返回值定义类型
ts
let a: number // 声明一个变量, 并指定它的类型为number
a = 10
a = 'hello' // 会有红线提示,不能将类型“string”分配给类型“number”。

// 声明并定义
let b : number = 1
b = true

// 实际上声明和赋值是同时进行的, ts可以自动对变量进行类型检测
let c = true
c = 1

// 为什么还要主动定义呢? 主要是函数的参数和返回类型
function sum (a: number, b: number): number{	// 括号后面的冒号表示返回的类型
  return a + b
}

sum(123, '456')   // 第二个类型不符合会报错

sum(123, 456, 789)  // 如果是js, 多传参并不会报错, 但js更严格, 不允许多传参

// 虽然报错了, 但还是会编译成js, 只是会在编译时报错, 如果想报错不编译, 需要配置

类型推断

类型声明并不是必须的, 编译器会分析上下文信息和表达式的结构,来推断变量或表达式的类型

ts
let x = 10; // 类型推断为 number

let y = 'hello'; // 类型推断为 string

// 支持从函数的返回值推断函数的类型

function add(a: number, b: number) {
  return a + b;
}

let result = add(1, 2); // 类型推断为 number

ts中的类型

js中的类型: 原始数据类型和对象类型

原始数据类型包括: boolean、number、string、null、undefined 以及 ES6 中的新类型 Symbol 和 ES10 中的新类型 BigInt。

对象类型包括: Array, Object, Function, Date, Regex ...

number 数值

ts
let decLiteral: number = 6;

// ES6 中的二进制表示法
let hexLiteral: number = 0xf00d;
// ES6 中的八进制表示法
let binaryLiteral: number = 0b1010;
let octalLiteral: number = 0o744;

let notANumber: number = NaN;
let infinityNumber: number = Infinity;

string 字符串

ts
let myName: string = 'Tom';

boolean 布尔值

ts
let isDone: boolean = false;

void 空值

JavaScript 没有空值的概念

ts
// 主要用在函数的返回值,表示没有返回值
function alertName(): void {
    alert('My name is Tom');
}

null和undefined

ts
// 与 void 的区别是,undefined 和 null 是所有类型的子类型
// 举例: undefined 类型的变量,可以赋值给 number 类型的变量
let u: undefined = undefined;
let n: null = null;
let num: number = u;

any 任意类型

所有类型的集合, 对该变量关闭ts检测(尽量不用)

ts
// 1显式声明
let e: any
e = 10 // 允许被赋值为任意类型
e = true

// 2. 隐式声明
let f // 声明变量如果不赋值, ts解析器就会自动当成any
f = 10
f = true

使用场景: 尽量不使用, 除非适配旧的js项目

**问题: 类型污染, 它可以赋值给其他任何类型的变量(因为没有类型检查),把错误留到运行时

解决: unknown

unkonwn 未知类型

未知类型

和any有什么区别:

  • any可以赋值给任意变量(==不仅影响自己, 还影响别人== )
  • unknow不能直接赋值给其他变量
  • unknow实际上就是一个==类型安全的any==
ts
let s1 : any
s1 = 123
let s2 : string
s2 = s1  // any可以赋值给任意变量, 这里不会报错

let s3 : unknown
s3 = 123
let s4 : string
s4 = s3  // unknow会报错

// unkown要怎么赋值给其他变量呢? 明确unkown的类型
// 方法1:先做类型判断
if(typeof s3 === 'string'){
  s4 = s3
}

// 方法2: 类型断言, 用来告诉解析器变量的实际类型
/*
 * 语法: 变量 as 类型
 *      <类型> 变量
 **/
s4 = s3 as string
s4 = <string>s3

never 无返回

  1. 不可能返回值的函数, 返回值的类型就可以写成never
  2. 类型运算之中,保证类型运算的完整性
ts
// 1. 异常时抛出无返回
function f():never {
  throw new Error('Error');
} 

let v1:number = f(); // 不报错, never可以赋值给任意其他类型
let v2:string = f(); // 不报错
let v3:boolean = f(); // 不报错

never 可以赋值给任意其他类型

对象的类型

ts
let a:object  //并不这么用, 因为js里数组 对象 函数都是对象
a = {}
a = function () {}

let b : {name:string, age?:number, [propName:string]:any}
// 一般指定对象包含的属性
// 语法: {属性名: 属性值类型, ...}
// 属性名后面的?:表示是可选的
// [propName:string]:any 索引签名, 属性名是string,属性值是any, 数量不限
b = {}
b = {name : 'abc'}

另一种是使用接口(interfaces)

ts
interface Person {
    name: string
    age?: number // ?可选属性
    [propName:string]:any  // 索引签名, 属性名是string,属性值是any, 数量不限
}

let tom: Person = {
    name: 'Tom',
    age: 25
};
  • type和interface的区别

用interface描述数据结构,用type描述类型关系 多次声明的同名 interface 会进行声明合并;type 不允许多次声明,一个作用域内不允许有多个同名 type

索引签名

批量描述key的一种方式

ts
let a : {[index:string]:any} // 这里的index没有任何意义

所有成员都必须符合索引签名

ts
type A = {
  [index:string]: number,
  x : number,
  y : string // 不能是string, 必须符合索引签名number
}

type B = {
  x : number,
  y : string, // 写在前面也不行,对象本来就没有顺序
  [index:string]: number
}

数组

ts
// 用来表示数组里存放什么类型的数据
// 有多种表示方法
// 1. 第一种: 类型加方括号, 数组中不允许出现其他类型
let a : string[]
let c : any[] // 数组里不做限制

// 2. 第二种: 数组泛型
let b : Array<any> // <> 里放的是类型

除了数组泛型, 常见的还有 函数泛型, 类泛型, 接口泛型, 类型别名泛型

[[#泛型]]

数组的类型推断

ts
let myFriends = ['Alex', 'Bob', 7]
// 被推断成联合类型
myFriends[0].slice() // Property'slice' does not exist on type 'string | number'

只读数组

在数组类型前面加上readonly关键字

ts
const arr:readonly number[] = [0, 1];

arr[1] = 2; // 报错

另一种写法: const 断言

ts
const arr = [0, 1] as const

只读数组是数组的父类型, 不能代替数组. 解决: 使用类型断言

ts
function getSum(s:number[]) {
  // ...
}

const arr:readonly number[] = [1, 2, 3];

getSum(arr) // 报错
getSum(arr as number[])

多维数组

T [][] 表示二维数组,T是最底层数组成员的类型

ts
let multi:number[][] = [[1,2,3], [23,24,25]]

类数组(Array-like Object)

==不是数组==, 比如 arguments

ts
// 不能这么写
function sum() {
    let args: number[] = arguments;
}
// arguments 实际上是一个类数组,不能用普通的数组的方式来描述,而应该用接口
ts
function sum() {
    let args: IArguments = arguments;
}
// 常用的类数组都有自己的接口定义,如 IArguments, NodeList, HTMLCollection
// IArguments 是 TypeScript 中定义好了的接口

元组

ts新增的类型, 元组, 成员类型可以自由设置的数组

ts
let a : [string , string]  // 长度固定, 效率更好
a = ['hello', 'word']

// 可选成员
// 可选成员必须在必选成员之后
type myTuple = [
  number,
  number,
  number?,
  string?
]

// 不限成员数量
// 使用扩展运算符 ...
type NamedNums = [
  string,
  ...number[]
]

// 只读元组
type t = readonly [number, string]

// const 断言
let point = [3, 4] as const

函数

js有两种常见的定义函数的方式: 函数声明 函数表达式

ts
// 函数声明的类型定义
function sum(x: number, y: number): number {
    return x + y;
}
ts
// 函数表达式的类型定义
let mySum = function (x: number, y: number): number {
    return x + y;
}
// 上面只对等号右边的匿名函数进行了类型定义,左边的是通过赋值推断出来的

// 如果要手动添加类型
let mySum: (x: number, y: number) => number = function (x: number, y: number): number {
    return x + y;
}

// 语法: (形参:类型, 形参:类型,...)=> 返回值类型
// => 这里和箭头函数区分开
ts
// 用接口定义函数的类型
interface SearchFunc {
    (source: string, subString: string): boolean;
}

let mySearch: SearchFunc;
mySearch = function(source: string, subString: string) {
    return source.search(subString) !== -1;
}

可选参数

==可选参数放最后, 后面不允许再出现必需参数==

ts
// 用?表示可选参数
function buildName(firstName: string, lastName?: string) {
    if (lastName) {
        return firstName + ' ' + lastName;
    } else {
        return firstName;
    }
}
let tomcat = buildName('Tom', 'Cat');
let tom = buildName('Tom');

参数默认值

ts
// TypeScript 会将添加了默认值的参数识别为可选参数
// 此时就不受「可选参数必须接在必需参数后面」的限制了
function buildName(firstName: string = 'Tom', lastName: string) {
    return firstName + ' ' + lastName;
}
let tomcat = buildName('Tom', 'Cat');
let cat = buildName(undefined, 'Cat');

重载

ts
// 重复定义了多次函数 reverse,前几次都是函数定义,最后一次是函数实现
// TypeScript 会优先从最前面的函数定义开始匹配,所以多个函数定义如果有包含关系,需要优先把精确的定义写在前面
function reverse(x: number): number;
function reverse(x: string): string;
function reverse(x: number | string): number | string | void {
    if (typeof x === 'number') {
        return Number(x.toString().split('').reverse().join(''));
    } else if (typeof x === 'string') {
        return x.split('').reverse().join('');
    }
}

断言

手动指定一个值的类型

ts
// 语法
as 类型
// 或者
<类型>值

// 在tsx中必须使用as, 因为<>被组件/标签用了

====类型断言只能够「欺骗」TypeScript 编译器,无法避免运行时的错误,反而滥用类型断言可能会导致运行时错误

将一个联合类型断言为其中一个类型

ts
// 有时候,我们确实需要在还不确定类型的时候就访问其中一个类型特有的属性或方法
interface Cat {
    name: string;
    run(): void;
}
interface Fish {
    name: string;
    swim(): void;
}

function isFish(animal: Cat | Fish) {
    if (typeof (animal as Fish).swim === 'function') {
        return true;
    }
    return false;
}

将一个父类断言为更加具体的子类

ts
interface ApiError extends Error {
    code: number;
}
interface HttpError extends Error {
    statusCode: number;
}

function isApiError(error: Error) {
    if (typeof (error as ApiError).code === 'number') {
        return true;
    }
    return false;
}

将任何一个类型断言为 any

js
// 给 window 上添加一个属性 foo
(window as any).foo = 1

将 any 断言为一个具体的类型

ts
// 通过类型断言及时的把 `any` 断言为精确的类型,亡羊补牢,使我们的代码向着高可维护性的目标发展
function getCacheData(key: string): any {
    return (window as any).cache[key];
}

interface Cat {
    name: string;
    run(): void;
}

const tom = getCacheData('tom') as Cat;
tom.run();

断言的限制

要使得 A 能够被断言为 B,只需要 A 兼容 B 或 B 兼容 A 即可. 这也是为了在类型断言时的安全考虑,毕竟毫无根据的断言是非常危险的

ts
// 为什么 person2可以赋值给person1 ?
let person1 : {name:string} = {name:'aaa'}
let person2 : {name:string,age:number} = {name:'bbb', age: 1}
person1 = person2
ts
// 在 TypeScript 中,类型检查是根据结构进行的,而不是根据名称。这意味着如果两个对象的结构相同,那么它们就可以互相赋值,即使它们的属性名称不同。
// person1 的类型是 {name: string},而 person2 的类型是 {name: string, age: number}。虽然它们的属性名称不同,但它们都有一个名为 name 的属性,因此它们的结构相同。
// person2 的类型具有比 person1 更多的属性,这意味着它可以包含 person1 的所有属性。在这种情况下,只会使用 person2 中的 name 属性,并忽略 age 属性。
ts
person2 = person1 // 这样就不行了, person1缺少age属性
ts
interface Animal {
    name: string;
}
interface Cat {
    name: string;
    run(): void;
}

function testAnimal(animal: Animal) {
    return (animal as Cat); // 父类可以被断言为子类,前面提到
}
function testCat(cat: Cat) {
    return (cat as Animal); // 子类可以被断言为父类, 因为既然子类拥有父类的属性和方法,那么被断言为父类,获取父类的属性、调用父类的方法,就不会有任何问题
}

枚举

用于取值被限定在一定范围内的场景,比如一周只能有七天,颜色限定为红绿蓝等

ts
// 枚举成员会被赋值为从 0 开始递增的数字,同时也会对枚举值到枚举名进行反向映射
enum Days {Sun, Mon, Tue, Wed, Thu, Fri, Sat};
console.log(Days["Sun"] === 0); // true
console.log(Days["Sat"] === 6); // true

console.log(Days[0] === "Sun"); // true
console.log(Days[6] === "Sat"); // true
ts
// 手动赋值
enum Days {Sun = 7, Mon = 1, Tue, Wed, Thu, Fri, Sat};
// 未手动赋值的枚举项会接着上一个枚举项递增

联合类型

|表示或

ts
type myType = 1 | 2 | 3 | 4 | 5
  • 只能访问联合类型所有类型里共有的属性或方法

交叉类型

&表示且

ts
let j : {name: string} & {age: number}
j = {name: '张三', age: 18}

类型别名

类型的别名, 就是给类型起一个新名字,支持基本类型、联合类型、元组及其它任何你需要的手写类型,常用于联合类型

建议参考官方的写法, 使用大驼峰

ts
type MyType = 1 | 2 | 3 | 4 | 5
let a : MyType
let b : MyType

type NameResolver = () => string; // 给一个函数类型起别名, = 左边是别名
  1. 和接口一样,用来描述对象或函数的类型
tsx
type User = {
    name: string
    age: number
};
type SetUser = (name: string, age: number)=>void;
  1. interface可以扩展,type可以通过交叉实现interface的extends行为
    interface可以extends type,同时type也可以与interface类型交叉
ts
interface Name {
  name: string;
}
interface User extends Name {
  age: number
}
let stu:User = {name: 'wang', age: 10}


//interface的扩展可以通过type交叉(&)类型实现
type Name = {
   name: string;
}
type User = Name & {age: number}
let stu:User={name: 'wang', age: 18}


//interface 扩展 type
type Name = {
  name: string;
}
interface User extends Name {
  age: number;
}
let stu:User={name: 'wang', age: 89}


//type与interface交叉
interface Name {
  name: string;
}
type User = Name & {
  age: number;
}
let stu:User={name:'wang', age: 18}
  1. type 能使用 in 关键字生成映射类型,但interface不行

    rust
    type Keys = "name" | "sex"
    type DuKey = {
      [Key in Keys]: string //类似 for ... in
    }
    let stu: Dukey = {
      name: 'wang'
      sex: 'man'
    }

字面量

ts
// type也可以用来表示字面量
// 类型别名是为现有的类型定义一个新的名称, 字面量是定义一个特定的值的类型
// 类型别名可以包含其他的类型,也可以使用泛型,而字面量只能表示特定的值或类型
// 类型别名可以用来表示复杂的类型,如联合类型、交叉类型等,而字面量主要用来表示基本类型,如字符串、数字、布尔值等
type EventNames = 'click' | 'scroll' | 'mousemove';

const stringLiteral = "https"; 
// const stringLiteral: "https"

let str = 'hello' as const

Exclude

是TS中的一个高级类型,其作用是从第一个联合类型参数中,将第二个联合类型中出现的联合项全部排除,只留下没有出现过的参数。

ts
type A = Exclude<'key1' | 'key2', 'key2'> // 'key1'

面向对象

  • 封装: 将对数据的操作细节隐藏起来,外界调用端不需要知道细节.
  • 继承: 子类继承父类,子类除了拥有父类的所有特性外,还有一些更具体的特性
  • 多态: 由继承而产生了相关的不同的类,对同一个方法可以有不同的响应

关键字class

和js一样

ts
enum Gender {
  woman = 0,
  man = 1
}
// 使用class关键字
/**
 * 对象包含两个部分
 * 1. 属性
 * 2. 方法
 */
class Person {
  // 定义实例属性
  name:string = "张三"
  age:number = 18
  // 在属性前使用static关键字可以定义类属性(静态属性)
  // 静态属性只能通过对象访问, 不能通过实例访问
  static height: number = 180
  // readonly只读属性, 不能改
  readonly gender: Gender = Gender.man

  // 定义方法
  // 实例方法
  sayHellow (){
    console.log('hello word')
  }
  // 同样在方法前加static可以定义类方法
  static sayHaha (){
    console.log('haha')
  }
}

const per = new Person()
console.log(per)
console.log(Person.height)

per.sayHellow()
Person.sayHaha()

构造函数construtor

和js一样

ts
// 实例的时候, 每个实例的属性肯定要灵活设置, 所以要用到构造函数
class Dog {
  name:string
  age:number

  // 构造函数在实例的时候被执行
  // 在一个类中只能有一个名为 “constructor” 的特殊方法
  constructor(name:string, age:number){
    console.log('构造函数被执行了');
    // this指向新实例的那个对象
    console.log(this)
    this.name = name
    this.age = age
  }

  bark(){
    // 在方法中this指向调用该方法的对象
    alert(this.name+':汪汪汪')
  }
}

const dog1 = new Dog("小白",1)
const dog2 = new Dog("小黑",2)
console.log(dog1)
dog1.bark()
console.log(dog2)
dog2.bark()

继承extends

js里的类的继承(和class一起用)

js
// 1. js类的继承
class Animal {
  kind = 'animal'
  constructor(kind){
    this.kind = kind;
  }
  sayHello(){
    console.log(`Hello, I am a ${this.kind}!`);
  }
}

class Dog extends Animal {
  constructor(kind){
    super(kind)
  }
  bark(){
    console.log('wang wang')
  }
}

const dog = new Dog('dog');
dog.name; //  => 'dog'
dog.sayHello(); // => Hello, I am a dog!

ts 类型的继承

ts
// 2. ts类型的继承
interface Animal {
   kind: string;
 }

 interface Dog extends Animal {
   bark(): void;
 }
 // Dog => { name: string; bark(): void }

ts 泛型约束

ts
// 3. 泛型约束
function getCnames<T extends { name: string }>(entities: T[]):string[] {
  return entities.map(entity => entity.cname)
}
// 对传入的参数作了一个限制,就是 entities 的每一项可以是一个对象,但是必须含有类型为string的cname属性

ts 条件类型

ts
// 4. 条件类型: 判断一个类型是不是可以分配给另一个类型
  type Human = {
    name: string;
  }
  type Duck = {
    name: string;
  }
  type Bool = Duck extends Human ? 'yes' : 'no'; // Bool => 'yes'

// 4.1
 type Human = {
    name: string;
    occupation: string;
  }
  type Duck = {
    name: string;
  }
  type Bool = Duck extends Human ? 'yes' : 'no'; // Bool => 'no'

ts 分配条件类型

ts
// 5. 分配条件类型:
// 对于使用extends关键字的条件类型(即上面的三元表达式类型),如果extends前面的参数是一个泛型类型,当传入该参数的是联合类型,则使用分配律计算最终的结果。
// 分配律是指,将联合类型的联合项拆成单项,分别代入条件类型,然后将每个单项代入得到的结果再联合起来,得到最终的判断结果。
type A1 = 'x' extends 'x' ? string : number; // string
type A2 = 'x' | 'y' extends 'x' ? string : number; // number

type P<T> = T extends 'x' ? string : number;
type A3 = P<'x' | 'y'> // ?
// P是一个泛型类型,接收一个泛型参数T. P<T>的定义中, 使用了条件类型, 如果如果T类型
// 根据分配条件类型: A3的类型是 string | number

// 特殊的never
// never是所有类型的子类型
type A1 = never extends 'x' ? string : number; // string

type P<T> = T extends 'x' ? string : number;
type A2 = P<never> // never
// never被认为是空的联合类型,也就是说,没有联合项的联合类型,所以还是满足上面的分配律,然而因为没有联合项可以分配,所以P<T>的表达式其实根本就没有执行,所以A2的定义也就类似于永远没有返回的函数一样,是never类型的

重写和调用父类

  • 属性的重写
  • 方法的重写
  • super的使用

==上面的都和js一样, 下面的抽象类接口是ts独有的, 编译成js就没了==

ts
// 如果我还有一个猫的类, 一个鸟的类, 每个类都要创建一遍, 太麻烦, 把公共的抽出来,变成一个公共类

class Animal {
name:string
age:number

constructor(name:string, age:number){
  this.name = name
  this.age = age
}

say(){
  console.log('动物在叫')
}
}

// 使Dog类继承Animal类
// Animal是父类, Dog是子类
// 继承后, 子类有父类的所有属性和方法
// 如果希望在子类中添加一些自己独有的属性和方法, 直接写
// 子类覆盖父类方法, 叫做方法的重写
class Dog extends Animal {
food: string = "骨头"
say(){
  console.log('汪汪汪')
}
}

class Cat  extends Animal {
say(){
  console.log("喵喵喵")
}
}

// super的使用场景: 构造函数中调用父类的构造函数
// 比如react中就用到了
class Bird extends Animal {
color:string

constructor(name:string, age:number,color:string){
  // 如果在子类中写了构造函数, 在子类构造函数中, 必须对父类的构造函数进行调用
  super(name,age)
  this.color = color
}
say(){
  // 在类的方法中, super就表示当前类的父类
  super.say()
}
}

const dog = new Dog("小白",1)
console.log(dog)
dog.say()

const cat = new Cat("小黑",2)
console.log(cat)
cat.say()

const bird = new Bird("小红",3, "红")
console.log(bird)
bird.say()

抽象类: abstrat

父类不想被实例

ts
// 抽象类: 不能用来创建对象, 就是专门用来被继承的父类
// 在class前面加上abstract
abstract class Animal {
name:string
age:number

constructor(name:string, age:number){
  this.name = name
  this.age = age
}

// 抽象方法: 定义在抽象类中, 只是表示了有这个方法, 必须在子类中进行重写
//在方法名前加 abstract , 没有方法体
abstract say():void
}

// 使Dog类继承Animal类
// Animal是父类, Dog是子类
// 继承后, 子类有父类的所有属性和方法
// 如果希望在子类中添加一些自己独有的属性和方法, 直接写
// 子类覆盖父类方法, 叫做方法的重写
class Dog extends Animal {
food: string = "骨头"
say(){
  console.log('汪汪汪')
}
}

class Cat  extends Animal {
say(){
  console.log("喵喵喵")
}
}

// super的使用场景: 构造函数中调用父类的构造函数
// 比如react中就用到了
class Bird extends Animal {
color:string

constructor(name:string, age:number,color:string){
  // 如果在子类中写了构造函数, 在子类构造函数中, 必须对父类的构造函数进行调用
  super(name,age)
  this.color = color
}
say(){
  // 在类的方法中, super就表示当前类的父类
  // super.say() // 这里没有重写就报错了
}
}

const dog = new Dog("小白",1)
console.log(dog)
dog.say()

const cat = new Cat("小黑",2)
console.log(cat)
cat.say()

const bird = new Bird("小红",3, "红")
console.log(bird)
bird.say()

接口: interface

接口用来定义一个类的结构, 应该包含那些属性和方法 接口就是对类的限制 也可以当成类型声明使用

类型声明

ts
// 定义一个对象的类型
type myType = {
name: string,
age: number
}

const obj:myType = {
name: '张三',
age: 18
}

// 1. 当成类型声明使用
interface myInterFace{
name: string
age: number
}

interface myInterFace{
height: number
}
//再定义一个同样名字的接口, 在ts里是合法的, 实际是两个合并

const obj2: myInterFace = {
name: '李四',
age: 19,
height: 180
}

定义类的结构

ts
// 2. 定义类的结构
// 接口中的属性都不能有实际的值
// 接口只是定义了对象的结构,而不考虑实际值
interface myInter {
name:string
say():void // 接口中的方法都是抽象方法
}

// 类实现接口, 用implements
class myClass implements myInter{
name: string
constructor(name: string){
  this.name = name
}
say (){
  console.log('123');
}
}
const myclass = new myClass('王五')
console.log(myclass.name);
myclass.say()

属性的封装: private

如果不想让属性被任意修改, 可以添加private 设置私有属性, 类内部添加get, set存取器修改

ts
// 实例属性在实例中是可以被任意修改的, 如果不想被修改或者要按照我的方法来修改
// 添加private 设置私有属性, 类内部添加修改方法
// tsconfig.json 中添加 "noEmitOnError":true, 错误时不编译

class Person{
/**
 * TS可以在属性前面添加修饰符, 比如之前的static
 * public 表示公共属性, 默认添加
 * private 表示私有属性, 只能在类内部访问/修改
 * protected 表示受保护的类, 只能在当前类和当前类的子类中访问
 *   可以通过类内部的方法间接修改
 */
 /**
 * js 有静态属性,静态方法 static
 * js 有私有字段, #    (ECMAScript 2022)
 */
private _name:string
private _age: number

constructor(name:string, age:number){
  this._name = name
  this._age = age
}

setName(value:string){
  this._name = value
}

setAge(value: number){
  if(value > 0 && value <150){
	// 在内部的方法添加了限制, 只有符合设置规则的修改才允许
	this._age = value
  }
}

/**
 * getter方法用来读取属性, setter方法用来设置属性, 它们被称为属性的存取器
 * TS中提供了更方便写法 set get
 * 用的时候当成属性来用
 */
get name():string{
  return this._name  // get访问器不能有参数, 必须用return返回
}

set name(value:string){
  this._name = value   // set访问器必须有参数, 不能有return
}
}

const per = new Person("张三", 18)
console.log(per);
//per._name = "李四"  // 报错了, 私有属性只能在类内部修改

per.setName('李四')
console.log(per)

per.setAge(-1)  // 不符合规则, 不让修改
console.log(per)

console.log(per.name); // 直接调用了get访问器
per.name = "王五"   // set访问器, 和属性一样的赋值, 而不是方法传参
console.log(per.name);

可以直接将属性定义在构造函数中

ts
class A {
    constructor(public name:string, public age:number){}
}

// 相当于
class A{
    name: string
    age: number
    constructor(name: string,age: number){}
}

泛型

泛型(Generics)是指在定义函数、接口或类的时候,不预先指定具体的类型,而在使用的时候再指定类型的一种特性。

常见的有数组泛型, 函数泛型, 类泛型, 接口泛型, 类型别名泛型

ts
// 数组泛型
let list : Array<any> = []
ts
// 定义函数或者类的时候, 如果类型不明确可以使用泛型
// 和any的区别, any是关闭了检查, 泛型是一个占位模板
function fn <T> (a:T) :T{
return a
}

console.log(fn(123))
console.log(fn('456'))

// 泛型可以设置多个
function fn2 <T, K> (a:T, b: K) :T{
console.log(b)
return a
}

let b = fn2(789, 'abc')
console.log(b);

// 泛型可以主动指定
fn2<string, number>('qwe', 996)

// 泛型继承接口, 就是说类型必须符合接口的格式
interface Inter{
length: number
}

function fn3<T extends Inter>(a:T) {
return a.length  // 类型和接口一样, 有lenght属性
}

// 泛型参数的默认类型
function createArray<T = string>(length: number, value: T): Array<T> {
    let result: T[] = [];
    for (let i = 0; i < length; i++) {
        result[i] = value;
    }
    return result;
}
ts
// 类泛型
class Box<T> {
    private value: T;
    constructor(value: T) {
        this.value = value;
    }
    getValue(): T {
        return this.value;
    }
}
ts
// 接口泛型
interface Pair<T, U> {
    first: T;
    second: U;
}
ts
// 类型别名泛型
type Container<T> = { value: T };

其他

声明语句

定义类型用于编译时检查,在编译结果中会被删除

ts
// 例如 引入jQuery
declare var jQuery: (selector: string) => any;
jQuery('#foo')
// 并没有真的定义一个变量,只是定义了全局变量 jQuery 的类型

声明文件

当使用第三方库时,我们需要引用它的声明文件,才能获得对应的代码补全、接口提示等功能 把声明语句放到一个单独的文件(jQuery.d.ts)中,这就是声明文件

ts
// src/jQuery.d.ts
declare var jQuery: (selector: string) => any;
// ts 会解析项目中所有的 *.ts 文件,当然也包含以 .d.ts 结尾的文件
// 其他所有 *.ts 文件就都可以获得 jQuery 的类型定义

使用社区已定义好的声明文件

sh
# 以jQuery为例
npm install @types/jquery --save-dev

写声明文件

全局变量

  • declare let/const 声明全局变量
  • declare function 声明全局方法
  • declare class 声明全局类
  • declare enum 声明全局枚举类型
  • interfacetype 声明全局类型
  • declare namespace 声明(含有子属性的)全局对象
  • /// <reference /> 三斜线指令
declare let/const

声明全局变量

ts
// src/jQuery.d.ts
declare let jQuery: (selector: string) => any;
// 使用 declare let 定义的 jQuery 类型,允许修改这个全局变量
// 使用 declare const 定义的 jQuery 类型,禁止修改这个全局变量
// 一般来说,全局变量都是禁止修改的常量,所以大部分情况都应该使用 const 而不是 var 或 let
ts
// src/index.ts
jQuery('#foo');
jQuery = function(selector) {
    return document.querySelector(selector);
};
declare function

声明全局方法

ts
// src/jQuery.d.ts
declare function jQuery(selector: string): any;
ts
// src/index.ts
jQuery('#foo')

函数重载

ts
// src/jQuery.d.ts
declare function jQuery(selector: string): any;
declare function jQuery(domReadyCallback: () => any): any;
ts
// src/index.ts
jQuery('#foo');
jQuery(function() {
    alert('Dom Ready!');
});
declare class

声明全局类

ts
// src/Animal.d.ts
declare class Animal {
    name: string;
    constructor(name: string);
    sayHi(): string;
}
ts
// src/index.ts
let cat = new Animal('Tom');
declare enum

声明全局枚举类型

ts
// src/Directions.d.ts
declare enum Directions {
    Up,
    Down,
    Left,
    Right
}
ts
// src/index.ts
let directions = [Directions.Up, Directions.Down, Directions.Left, Directions.Right]
interfacetype

声明全局类型

ts
// src/jQuery.d.ts
interface AjaxSettings {
    method?: 'GET' | 'POST'
    data?: any;
}
declare namespace jQuery {
    function ajax(url: string, settings?: AjaxSettings): void;
}
ts
// src/index.ts
let settings: AjaxSettings = {
    method: 'POST',
    data: {
        name: 'foo'
    }
};
jQuery.ajax('/api/post_something', settings);
declare namespace

声明(含有子属性的)全局对象

namespace: 命名空间, ts早期解决模块化而创造的关键字, 现在推荐使用 ES6 的模块化方案

声明文件中,declare namespace 还是比较常用的,它用来表示全局变量是一个对象,包含很多子属性

ts
// src/jQuery.d.ts
declare namespace jQuery {
    function ajax(url: string, settings?: any): void; // 内部就不用declare了
}
ts
// src/index.ts
jQuery.ajax('/api/get_something');

命名空间还可以嵌套

ts
// src/jQuery.d.ts
declare namespace jQuery {
    function ajax(url: string, settings?: any): void;
    namespace fn {
        function extend(object: any): void;
    }
}
ts
// src/index.ts
jQuery.ajax('/api/get_something');
jQuery.fn.extend({
    check: function() {
        return this.each(function() {
            this.checked = true;
        });
    }
});

只有一个属性, 也可以这么写

ts
// src/jQuery.d.ts
declare namespace jQuery.fn { // 用.
    function extend(object: any): void;
}
ts
// src/index.ts
jQuery.fn.extend({
    check: function() {
        return this.each(function() {
            this.checked = true;
        });
    }
});
三斜线指令

三斜线指令必须放在文件的最顶端,三斜线指令的前面只允许出现单行或多行注释

使用场景:

  • 书写全局变量的声明文件
  • 依赖一个全局变量的声明文件
  1. 书写全局变量的声明文件:
ts
/// <reference types="jquery" />

declare function foo(options: JQuery.AjaxSettings): string;

/// 后面使用 xml 的格式添加了对 jquery 类型的依赖,这样就可以在声明文件中使用 JQuery.AjaxSettings 类型了。

  1. 依赖一个全局变量的声明文件
ts
/// <reference types="node" />

export function foo(p: NodeJS.Process): string;

通过三斜线指引入了 node 的类型,然后在声明文件中使用了 NodeJS.Process 这个类型。

  1. 拆分声明文件
ts
/// <reference types="sizzle" />
/// <reference path="JQueryStatic.d.ts" />
/// <reference path="JQuery.d.ts" />
/// <reference path="misc.d.ts" />
/// <reference path="legacy.d.ts" />

export = jQuery;

全局变量的声明文件太大时,可以通过拆分为多个文件,然后在一个入口文件中将它们一一引入,来提高代码的可维护性

npm包

假设通过import test from 'test'导入一个npm包

  1. 先找npm包是否已存在声明文件
    1. package.json中有types字段
    2. index.d.ts文件
    3. 安装@types包, 尝试安装npm i @types/test -D
  2. 如果都没有, 创建types/test/index.d.ts来管理, 并且需要配置tsconfig.jsonpathsbaseUrl字段
json
// tsconfig.json
{
    "compilerOptions": {
        "module": "commonjs",
        "baseUrl": "./",
        "paths": {
            "*": ["types/*"]
        }
    }
}
  • export 导出变量
ts
// types/test/index.d.ts
export const name: string;
export function getName(): string;
export class Animal {
    constructor(name: string);
    sayHi(): string;
}
export enum Directions {
    Up,
    Down,
    Left,
    Right
}
export interface Options {
    data: any;
}
ts
// 导入
import { name, getName, Animal, Directions, Options } from 'test';

也可以使用decalare声明,最后再导出

ts
// types/test/index.d.ts
declare const name: string;
declare function getName(): string;
declare class Animal {
    constructor(name: string);
    sayHi(): string;
}
declare enum Directions {
    Up,
    Down,
    Left,
    Right
}
interface Options {
    data: any;
}

export { name, getName, Animal, Directions, Options };
  • export default
ts
// types/test/index.d.ts
// 默认导出一般放声明文件在最前面
export default function test(): string;
ts
// src/index.ts
import test from 'test';

test();
ts
// 只有 function、class 和 interface 可以直接默认导出,其他的变量需要先定义出来,再默认导出
// types/test/index.d.ts
declare enum Directions {
    Up,
    Down,
    Left,
    Right
}

export default Directions;
  • commonjs 规范
ts
// types/test/index.d.ts
export = test;

declare function test(): string;
declare namespace test {  // 通过声明合并, 将 bar 合并到 test 里
    const bar: number;
}
ts
// 整体导入
const test = require('test');
// 单个导入
const bar = require('test').bar;

// 另一种导入
// 整体导入
import * as foo from 'test';
// 单个导入
import { bar } from 'test';

扩展全局变量

有的第三方库扩展了一个全局变量,可是此全局变量的类型却没有相应的更新过来,就会导致 ts 编译错误,此时就需要扩展全局变量的类型

  1. 声明合并
ts
// 给 String 添加属性或方法
interface String {
    prependHello(): string;
}

'foo'.prependHello();
  1. declare namespace 给已有的命名空间添加类型声明
ts
// types/jquery-plugin/index.d.ts

declare namespace JQuery {
    interface CustomOptions {
        bar: string;
    }
}

interface JQueryStatic {
    foo(options: JQuery.CustomOptions): string;
}
ts
// src/index.ts

jQuery.foo({
    bar: ''
});
  1. declare golbal 给npm包扩展全局变量的类型
ts
// types/test/index.d.ts
declare global {
    interface String {
        prependHello(): string;
    }
}

export {};
ts
// src/index.ts
'test'.prependHello();

自动生成声明文件

如果库的源码本身就是由 ts 写的,那么在使用 tsc 脚本将 ts 编译为 js 的时候,添加 declaration 选项,就可以同时也生成 .d.ts 声明文件

json
// tsconfig.json
{
    "compilerOptions": {
        "module": "commonjs",
        "outDir": "lib",
        "declaration": true,
    }
}

内置对象

JavaScript 中有很多内置对象,它们可以直接在 TypeScript 中当做定义好了的类型 内置对象是指根据标准在全局作用域(Global)上存在的对象。这里的标准是指 ECMAScript 和其他环境(比如 DOM)的标准。

ECMAScript

BooleanErrorDateRegExp 等。

ts
let b: Boolean = new Boolean(1);
let e: Error = new Error('Error occurred');
let d: Date = new Date();
let r: RegExp = /[a-z]/;

更多的内置对象,可以查看 MDN 的文档

DOM 和 BOM 的内置对象

DocumentHTMLElementEventNodeList 等。

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

Node.js不是内置对象的一部分

sh
npm install @types/node --save-dev

声明合并

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

  • 函数重载
ts
function reverse(x: number): number;
function reverse(x: string): string;
function reverse(x: number | string): number | string {
    if (typeof x === 'number') {
        return Number(x.toString().split('').reverse().join(''));
    } else if (typeof x === 'string') {
        return x.split('').reverse().join('');
    }
}
  • 接口的合并
ts
interface Alarm {
    price: number;
}
interface Alarm {
    weight: number;
}

// 相当于
interface Alarm {
    price: number;
    weight: number;
}

合并的属性的类型必须是唯一的

ts
interface Alarm {
    price: number;
}
interface Alarm {
    price: string;  // 类型不一致,会报错
    weight: number;
}

接口中方法的合并,与函数的合并一样:

ts
interface Alarm {
    price: number;
    alert(s: string): string;
}
interface Alarm {
    weight: number;
    alert(s: string, n: number): string;
}

// 相当于
interface Alarm {
    price: number;
    weight: number;
    alert(s: string): string;
    alert(s: string, n: number): string;
}