一文带你入门TypeScript

1,088 阅读13分钟

前言

原来学习过TypeScript,但很长一段时间没有使用啦,最近公司项目可能需要使用TypeScript的缘故,所以自己重新参考了很多文章以及看了一些ts视频,把一些基础的知识点全部复习并且总结了一下,希望对大家也有帮助吧

搭建TypeScript学习环境

在学习过程中我们可以利用ts-node去搭建ts环境 首先安装最新版TypeScript

npm install -g typescript

安装ts-node

npm install -g ts-node

创建一个tsconfig.json

tsc -- init

然后新建index.ts,输入相关练习代码,然后执行 ts-node index.ts

TS类型

基础数据类型

  • 常用:boolean、number、string、array、object、enum、any、void、unknown
  • 不常用:tuple、null、undefined、never

布尔类型

const flag: boolean = true;

Number 类型

const flag:number = 1;

Bigint 类型

使用 BigInt 可以安全地存储和操作大整数 我们在使用 BigInt 的时候 必须添加 ESNext 的编译辅助库 需要在 tsconfig.json 的 libs 字段加上 ESNext 要使用 1n需要 "target": "ESNext"

let big: bigint =  100n;

思考number和bigint的区别

在javaScript中Number是双精度浮点数,它可以表示的最大安全范围是正负9007199254740991,也就是2的53次方减一; 而BigInt是JavaScript中的一个新的原始类型,可以用任意精度表示整数。使用BigInt,即使超出JavaScript Number 的安全整数限制,也可以安全地存储和操作大整数。 chrome 67+开始支持BigInt。可以这样定义一个 BigInt 变量:在一个整数字面量后面加 n,如:10n ​

注意:虽然number和bigint都表示数字,但是这两个类型不兼容,通俗的来讲就是定义类型为number和bigint的变量是不能相互赋值的。

let num1:number = 1;
let num2:bigint = 200;
num1 = num2 // Error

Enum 枚举类型

使用枚举我们可以很好的描述一些特定的业务场景,比如错误码等

// 普通枚举 初始值默认为 0 其余的成员会会按顺序自动增长 可以理解为数组下标
enum Color {
  RED,
  PINK,
  BLUE,
}

const pink: Color = Color.PINK;
console.log(pink); // 1

// 设置初始值
enum Color {
  RED = 10,
  PINK,
  BLUE,
}
const pink: Color = Color.PINK;
console.log(pink); // 11

// 字符串枚举 每个都需要声明
enum Color {
  RED = "红色",
  PINK = "粉色",
  BLUE = "蓝色",
}

const pink: Color = Color.PINK;
console.log(pink); // 粉色

// 常量枚举 它是使用 const 关键字修饰的枚举,常量枚举与普通枚举的区别是,整个枚举会在编译阶段被删除 我们可以看下编译之后的效果

const enum Color {
  RED,
  PINK,
  BLUE,
}

const color: Color[] = [Color.RED, Color.PINK, Color.BLUE];

//编译之后的js如下:
var color = [0 /* RED */, 1 /* PINK */, 2 /* BLUE */];
// 可以看到我们的枚举并没有被编译成js代码 只是把color这个数组变量编译出来了

null和undefined

默认情况下null和undefined是所有类型的子类型,所以你可以将null和undefined赋给任意类型都是可以的。

// null和undefined赋值给string
let str:string = "666";
str = null
str= undefined

// null和undefined赋值给number
let num:number = 666;
num = null
num= undefined

// null和undefined赋值给object
let obj:object ={};
obj = null
obj= undefined

// null和undefined赋值给Symbol
let sym: symbol = Symbol("me"); 
sym = null
sym= undefined

// null和undefined赋值给boolean
let isDone: boolean = false;
isDone = null
isDone= undefined

// null和undefined赋值给bigint
let big: bigint =  100n;
big = null
big= undefined

如果你在tsconfig.json指定了"strictNullChecks":true ,null 和 undefined 只能赋值给 void 和它们各自的类型。 ​

Array

数组类型的定义方式有两种:

let arr:string[] = ["1","2"];
let arr2:Array<string> = ["1","2"];

定义联合类型数组

// 这个数组既可以存储数值类型的数据,也可以存储字符串类型的数据
let arr:(string | number)[] = [23, 'string']

定义指定对象成员的数组

interface Arrobj{
  name:string,
  age:number
}
// 接口Arrobj定义了数组内的键值只能为name、age
let arr:Arrobj[] = [{name: 'hzx', age: 23}]

元组类型(tuple)

在 TypeScript 的基础类型中,元组( Tuple )表示一个已知数量类型的数组 其实可以理解为他是一种数组里面每一项都需要定义类型的特殊数组

const flag: [string, number] = ["hello", 1];

Symbol

Symbol类型是es6新引入的类型,表示独一无二的值,最大的用法是用来定义对象的唯一属性名,相同参数 Symbol() 返回的值不相等, 在TypeScript内使用Symbol类型需要先在tsconfig.json的 libs 字段加上ES2015

{
  "compilerOptions": {
    "target": "es2016",
    "lib":["ES2015"]  
  }
}

any (任意类型)

任何类型都可以被归为 any 类型 这让 any 类型成为了类型系统的 顶级类型 (也被称作 全局超级类型) TypeScript 允许我们对 any 类型的值执行任何操作 而无需事先执行任何形式的检查 一般使用场景: 当数据结构过于复杂或者使用第三方库的时候不清楚类型的时候,不过也不能过于依赖any,不要把TypeScript玩成了anyScript,这样就违背了使用TypeScript的初衷,请酌情使用

const flags:any = '6666'
const arr:any = [1,2,4]
const childrenNode = document.querySelectorAll('#app')

Unknown 类型

let value: unknown;
value = true; // OK
value = 42; // OK
value = "Hello World"; // OK
value = []; // OK
value = {}; // OK

unknown和any的区别

unknowm 和 any 一样都是顶级类型,都是任意类型,但是Unknown 比 any要严格,可以理解为any的安全版本,区别在与unkown不像any那样不做类型检查,反而 unknown 因为未知性质,不允许访问属性,不允许赋值给其他有明确类型的变量 例如:

let foo: any = 123;
console.log(foo.msg); // 符合TS的语法
let a_value1: unknown = foo;   // OK
let a_value2: any = foo;      // OK
let a_value3: string = foo;   // OK


let bar: unknown = 222; // OK 
// 因为bar是一个未知类型(任何类型的数据都可以赋给 unknown 类型),所以不能确定是否有msg属性,所以不能通过TS语法检测
console.log(bar.msg); // Error
let k_value1: unknown = bar;   // OK
let K_value2: any = bar;      // OK
let K_value3: string = bar;   // Error

联合类型中的unkown

在联合类型中,unkown会吸收其他类型,这就意味着如果任一组成员类型是 unknown,联合类型也会相当于 unknown

type unionType1 = unknown | null;       // unknown
type unionType2 = unknown | undefined;  // unknown
type unionType3 = unknown | string;     // unknown
type unionType4 = unknown | number[];   // unknown

注意:但当任一组成员类型中同时存在unkown和any的时候,联合类型相当于any

type unionType = unknown | any // any

void 类型

void 表示没有任何类型 当一个函数没有返回值时 ts 会认为它的返回值是 void 类型。

const setName = ():void => {
  console.log('void执行')
  return true // Error
}

never 类型

never 一般不常用的类型,如never 类型是那些总是会抛出异常或根本就不会有返回值的函数表达式或箭头函数表达式的返回值类型

const neverReach = (): never => {
  throw new Error("an error");
}

object, Object 和 {} 类型

object 类型用于表示非原始类型

let objectCase: object;
objectCase = 1; // error
objectCase = "a"; // error
objectCase = true; // error
objectCase = null; // error
objectCase = undefined; // error
objectCase = {}; // ok

Object 代表所有拥有 toString、hasOwnProperty 方法的类型 所以所有原始类型、非原始类型都可以赋给 Object(严格模式下 null 和 undefined 不可以)

let ObjectCase: Object;
ObjectCase = 1; // ok
ObjectCase = "a"; // ok
ObjectCase = true; // ok
ObjectCase = null; // error
ObjectCase = undefined; // error
ObjectCase = {}; // ok

{} 空对象类型和Object 一样 也是表示原始类型和非原始类型的集合

let simpleCase: {};
simpleCase = 1; // ok
simpleCase = "a"; // ok
simpleCase = true; // ok
simpleCase = null; // error
simpleCase = undefined; // error
simpleCase = {}; // ok

类型推论

在TS中定义变量时未赋值就会推论成 any 类型 如果定义类型时就赋值则会自动推导出值的类型

const flag; // 推断为any
const age = 123; // 推断为number类型
const name = "hzx"; // 推断为string类型

联合类型

联合类型(Union Types)表示取值可以为多种类型中的一种 未赋值时联合类型上只能访问两个类型共有的属性和方法 (null or undefined )

let name: string | number;
console.log(name);
name = 1;
console.log(name.toFixed(2));
name = "hello";
console.log(name.length);

类型断言

在实际开发的时候,你清楚的了解某些值的详细信息,这个时候你可以通过断言让TS按照你断言的那个类型进行编译 (断言在实际开发中作用很大,可以帮我们避免很多编译报错)

// 类型断言分两种方式:
    
// 尖括号 语法
let someValue: any = "this is a string";
let strLength: number = (<string>someValue).length;

// as 语法
let someValue: any = "this is a string";
let strLength: number = (someValue as string).length;

注意:以上两种方式虽然没有任何区别,但是尖括号格式会与 react 中 JSX 产生语法冲突,因此我们更推荐使用 as 语法。

非空断言 在上下文中当类型检查器无法断定类型时 一个新的后缀表达式操作符 ! 可以用于断言操作对象是非 null 和非 undefined 类型

let flag: null | undefined | string;
flag!.toString(); // ok
flag.toString(); // error

字面量类型

在 TS 中,字面量不仅可以表示值,还可以表示类型,即所谓的字面量类型。 目前,TypeScript 支持 3 种字面量类型: 字符串字面量类型、数字字面量类型、布尔字面量类型 对应的字符串字面量、数字字面量、布尔字面量分别拥有与其值一样的字面量类型,具体示例如下:

let flag1: "hello" = "hello";
let flag2: 1 = 1;
let flag3: true = true;

类型别名

在TS中,类型别名常用来给一个类型取一个新的名字(ps: 联合类型建议使用类型别名重新定义一个名称)

type flag = string | number;

function setPrice(value: flag) {}

交叉类型

在TS中通过 & 运算符可以将现有的多种类型叠加到一起成为一种类型,这种类型就是交叉类型,它包含了叠加所有类型的所有特性

type Flag1 = { x: number };
type Flag2 = Flag1 & { y: string };

let flag3: Flag2 = {
  x: 1,
  y: "hello",
  henb, // Error 因为没定义所以这里报错
};

类型保护

类型保护一般用于在编译的时候就能通过检测属性、方法或原型确定某个作用域内变量的类型,来决定如何处理值 typeof 类型保护

type inputType = string | number | boolean
const double = (input: inputType) => {
  if (typeof input === "string") {
    return input + input;
  } else {
    if (typeof input === "number") {
      return input * 2;
    } else {
      return !input;
    }
  }
}

in 关键字

interface Bird {
  fly: number;
}

interface Dog {
  leg: number;
}

type valueType = Bird | Dog
function getNumber(value: valueType) {
  if ("fly" in value) {
    return value.fly;
  }
  return value.leg;
}
console.log('Bird fly', getNumber({fly: 1})) // Bird fly 1
console.log('Bird leg', getNumber({leg: 2})) // Bird leg 2

instanceof 类型保护

class Animal {
  name!: string;
}
class Bird extends Animal {
  fly!: number;
}
function getName(animal: Animal) {
  if (animal instanceof Bird) {
    console.log(animal.fly);
  } else {
    console.log(animal.name);
  }
}

自定义类型保护 通过 type is xxx这样的类型谓词来进行类型保护 例如下面的例子 value is object就会认为如果函数返回 true 那么定义的 value 就是 object 类型

const isObject = (value: unknown): value is object => {
  return typeof value === "object" && value !== null;
}

function fn(x: string | object) {
  if (isObject(x)) {
    // ....
  } else {
    // .....
  }
}

函数

函数声明

const sum = (age: number, money: number):number => {
return age * money
}

函数表达式

let mySum:(x:number, y:number) => number = (x:number, y:number) => {
  return x + y
}

采用函数表达式接口定义函数的方式时,对等号左侧进行类型限制,可以保证以后对函数名赋值时保证参数个数、参数类型、返回值类型不变。 ​

接口定义函数类型

// 注意,接口名首字母要用大写
interface SearchFunc{
(source:string, subString:string): bolean
}

const searchFun:searchFunc = (source, subString) {
  return true
}

可选参数

// 如果同一个接口根据传参做不同处理的时候,
// 可以通过<参数名>?:<参数类型>的方式定义一个可选参数,当传了参数的数据才校验类型
const getName = (name: string, age?: number) => {
  if(age){
    return `${name}${age}岁`
  } else {
    return name
  }
}
console.log(getName('小明')) // 小明
console.log(getName('小明',1000)) // 小明1000岁

注意点:可选参数后面不允许再出现必需参数,所以可选参数必须放在最后 ​

参数默认值

const getName = (name: string, age: number = 1000) => {
  return `${name}${age}岁`
}
console.log(getName('小明')) // 小明1000岁
console.log(getName('小明',2000)) // 小明2000岁

剩余参数

// 当不确定传参的时候可以使用剩余参数,将传入的参数收集到一个变量
const push = (array: any[], ...items: any[]) => {
  items.forEach((item) => {
    array.push(item);
  });  
}
let arr = [];
push(arr, 1,2,3,4,5)

注意:剩余参数会被当做个数不限的可选参数。 可以一个都没有,同样也可以有任意个

函数重载

在 TS 中如果想要同一个函数可以根据不同的参数去执行不同的参数,这时候就可以使用函数重载

let obj: any = {};
function attr(val: string): void;
function attr(val: number): void;
function attr(val: any): void {
  if (typeof val === "string") {
    obj.name = val;
  } else {
    obj.age = val;
  }
}
attr("hahaha");
attr(9);
attr(true); // Error
console.log(obj)

注意:函数重载真正执行的是同名函数最后定义的函数体 在最后一个函数体定义之前全都属于函数类型定义 不能写具体的函数实现方法 只能定义类型

类的定义

在 TypeScript 中,我们可以通过 Class 关键字来定义一个类

class Person {
  name!: string; //如果初始属性没赋值就需要加上!
  constructor(_name: string) {
    this.name = _name;
  }
  getName(): void {
    console.log(this.name);
  }
}
const person = new Person("zy");
person.getName();

注意:当我们定义一个类的时候,会得到 2 个类型 一个是构造函数类型的函数类型(当做普通构造函数的类型) 另一个是类的实例类型(代表实例)

例如:

class Component {
  static myName: string = "静态名称属性";
  myName: string = "实例名称属性";
}
//ts 一个类型 一个叫值
// 在 = 后面的是值
let com = Component; //这里是代表构造函数
//冒号后面的是类型
let c: Component = new Component(); //这里是代表实例类型

存取器

在TS中的类中,我们可以通过设置存取器来控制属性的赋值和读取行为

class Person {
  myName: string;
  constructor(name: string) {
    this.myName = name
  }
  get name() { // 读取
    return this.myName
  }
  set name(name: string) { // 赋值
    this.myName =  name
  }
}

let person = new Person('这就是存取器')
console.log('get',person.name) // 这就是存取器
person.name = '这是存取器的set'
console.log('set',person.name) // 这就是存取器的set

readonly 只读属性

在类中,readonly 修饰的变量只能在constructor中初始化 TS 的类型系统同样也允许将 interface、type、 class 上的属性标识为 readonly

class Animal {
  public readonly name: string;
  constructor(name: string) {
    this.name = name;
  }
  setName(name: string) {
    this.name = name; // 这个ts是报错的
  }
}
let a = new Animal("只读");

继承

在TS中如果两个类都有同一些方法和属性的情况下,一般为了提高代码的可复用性,我们会将子类的公共方法抽离出来作为父类,然后在子类中写子类的特殊处理,super 可以调用父类上的方法和属性,通过extends 关键字来实现继承父类

class User {
  name: string
  age: number
  constructor(name: string, age: number) {
    this.name = name
    this.age = age
  }
  getName(): string {
    return this.name
  }
  getAge(): number {
    return this.age
  }
}

class Info extends User {
  balance: number
  constructor(name: string, age: number,money: number) {
    super(name, age)
    this.balance = money
  }
  getBalance(): number {
    return this.balance
  }
}

let userInfo = new Info('weinan',2000,10000)
console.log(userInfo) // { name: 'hzx', age: 2000, balance: 10000 }
console.log('userInfo',userInfo.getName()) // hzx
console.log('userInfo',userInfo.getAge()) // 2000
console.log('userInfo',userInfo.getBalance()) // 10000

类的修饰符

类的修饰符分为三种: public 如何地方都可以访问,包括类、子类和类外面 (ps: 一般未声明的话默认为public) protected 类里面和子类可以访问,除此以外其他地方皆无法访问 private 只能在类里面访问,子类和其他地方也皆无法访问

class Parent {
  public name: string;
  protected age: number;
  private car: number;
  constructor(name: string, age: number, car: number) {
    //构造函数
    this.name = name;
    this.age = age;
    this.car = car;
  }
  getName(): string {
    return this.name;
  }
  setName(name: string): void {
    this.name = name;
  }
}
class Child extends Parent {
  constructor(name: string, age: number, car: number) {
    super(name, age, car);
  }
  desc() {
    console.log(`${this.name} ${this.age} ${this.car}`); //car访问不到 会报错
  }
}

let child = new Child("hello", 10, 1000);
console.log(child.name);
console.log(child.age); //age访问不到 会报错
console.log(child.car); //car访问不到 会报错

静态属性 静态方法

类的静态属性和方法是直接定义在类本身上面的 所以也只能通过直接调用类的方法和属性来访问

class Parent {
  static mainName = "Parent";
  static getmainName() {
    console.log(this);
    return this.mainName;
  }
  public name: string;
  constructor(name: string) {
    this.name = name;
  }
}
console.log(Parent.mainName);
console.log(Parent.getmainName());

注意静态方法里面的this指向的是类本身 而不是类的实例对象 所以静态方法里面只能访问类的静态属性和方法

抽象类和抽象方法

抽象类与抽象方法的区别如下: 抽象类,无法被实例化,只能被继承并且无法创建抽象类的实例,但是子类可以创建实例 抽象方法只能出现在抽象类中并且抽象方法不能在抽象类中被具体实现,只能在抽象类的子类中实现(ps: 必须要实现) 在实际开发中,我们一般用抽象类和抽象方法抽离出事物的共性 然后所有继承的子类必须按照规范去实现自己的具体逻辑 这样可以增加代码的可维护性和复用性 使用 abstract 关键字来定义抽象类和抽象方法

abstract class Users {
  name: string;
  abstract getName(): void;
}

class Infos extends Users {
  constructor(name: string) {
    super(); // 如果父类为抽象类,而子类需要创建实例需要添加super方法
    this.name = name
  }
  getName() {
    console.log("喵",this.name);
  }
}
let userInfo = new Infos('hzx');
userInfo.getName()

重写(override)和重载(overload)

重写 是指子类重写继承自父类中的方法 重载 是指为同一个函数提供多个类型定义

class Animal {
  speak(word: string): string {
    return "动物:" + word;
  }
}
class Cat extends Animal {
  speak(word: string): string {
    return "猫:" + word;
  }
}
let cat = new Cat();
console.log(cat.speak("hello"));
// 上面是重写
//--------------------------------------------
// 下面是重载
function double(val: number): number;
function double(val: string): string;
function double(val: any): any {
  if (typeof val == "number") {
    return val * 2;
  }
  return val + val;
}

let r = double(1);
console.log(r);

接口

接口定义

接口既可以在面向对象编程中表示为行为的抽象,也可以用来描述对象的形状 我们用 interface 关键字来定义接口 在接口中可以用分号或者逗号分割每一项,也可以什么都不加

interface User {
  readonly name: string;
  age: number;
  sex?: number;
}

let userInfo: User = {
  name: 'hzx',
  age: 1000,
  email: '', // 多出变量 or 少了变量 都会报错
}

行为的抽象

接口可以把一些类中共有的属性和方法抽象出来,可以用来约束实现此接口的类 一个类可以实现多个接口,一个接口也可以被多个类实现 我们用 implements关键字来代表 实现

//接口可以在面向对象编程中表示为行为的抽象
interface Speakable {
  speak(): void;
}
interface Eatable {
  eat(): void;
}
//一个类可以实现多个接口
class Person implements Speakable, Eatable {
  speak() {
    console.log("Person说话");
  }
  //   eat() {} //需要实现的接口包含eat方法 不实现会报错
}

定义任意属性

使用场景: 当我们在定义接口的时候如果我们不确定是否有其他属性的时候,我们可以通过[propName:string]:any 来设置任意属性,propName:是名字,可以随便取名的

interface A {
  name: string;
  age: number;
  [propName:string]:any;
}

let userInfo:A = {
  name: 'hzx',
  age: 1000,
  sex: '男'
}

接口的继承

我们除了类可以继承 接口也可以继承 同样的使用 extends关键字

interface User {
  getName(): string;
}

interface Info extends User {
  getAge(): number
}
class UserInfo implements Info {
  name: string;
  age: number;
  constructor(name: string, age: number) {
    this.name = name;
    this.age = age;
  }
  getName() { return this.name; } // 缺了这个函数就会报错
  getAge() { return this.age; } // 缺了这个函数就会报错
}

let userInfo = new UserInfo('weinan',1);
console.log('姓名',userInfo.getName()) // 姓名 weinan
console.log('年龄',userInfo.getAge()) // 年龄 1

函数类型接口

我们在TS中还可以用接口来给函数定义类型

interface Info {
  (name: string, age: number):string
}
const userInfo:Info = (name:string, age:number):string => {
  return `name:${name},age:${age}`
}
console.log(userInfo('weinan',1)) // name:weinan,age:1

构造函数的类型接口

在TS中,当我们需要把一个类作为参数的时候 我们需要对传入的类的构造函数类型进行约束 所以需要使用 new 关键字代表是类的构造函数类型 用以和普通函数进行区分

class Animal {
  name: string;
  constructor(name:string) {
    this.name = name
  }
}
// 不加new是修饰函数的,加new是修饰类的
interface WithNameClass {
  new (name: string): Animal;
}
const createAnimal = (clazz: WithNameClass, name: string) => {
  return new clazz(name);
}
let a = createAnimal(Animal, "hello zy");
console.log(a.name);

接口和类型别名的区别

在TS中,大多数情况下接口和类型别名的效果等价,但是在一些特定情况下两者还是有区别的: 1、type 可以声明 基本类型 别名、 联合类型元组 等类型、而 interface 不行

type MyType1 = boolean;
type MyType2 = string | number;
type MyType3 = [string, boolean, number];

2、重复定义 接口可以定义多次 会被自动合并为单个接口 类型别名不可以重复定义

interface Point {
  x: number;
}
interface Point {
  y: number;
}
const point: Point = { x: 1, y: 2 };

3、扩展方式 接口的扩展方式是继承,通过 extends 来实现。 类型别名的扩展方式就是交叉类型,通过 & 来实现

// 接口扩展接口
interface PointX {
  x: number;
}

interface Point extends PointX {
  y: number;
}
// ----
// 类型别名扩展类型别名
type PointX = {
  x: number;
};

type Point = PointX & {
  y: number;
};
// ----
// 接口扩展类型别名
type PointX = {
  x: number;
};
interface Point extends PointX {
  y: number;
}
// ----
// 类型别名扩展接口
interface PointX {
  x: number;
}
type Point = PointX & {
  y: number;
};

注意: 类无法实现定义了联合类型的类型别名

type PartialPoint = { x: number } | { y: number };

class SomePartialPoint implements PartialPoint {
  // Error
  x = 1;
  y = 2;
}

泛型

基本使用

泛型是指在定义类、接口、函数的时候不先预先定义类型,而且是在调用的时候再去定义类型的一种特性 打个比方:我们需要定义一个我们预先不知道会传入什么类型 但是我们希望不管我们传入什么类型,返回一个长度为length而里面的值的类型应该和参数保持一致的数组,这个时候我们就可以使用<>的写法 然后再面传入一个变量 T (ps: 这个变量是随便取的) 用来表示后续函数需要用到的类型 当我们真正去调用函数的时候再传入 T 的类型就可以解决很多预先无法确定类型相关的问题

const setArray = <T>(length: number, value: T):any[] => {
  let arr = [];
  for(let i = 0; i < length; i++) {
    arr.push(value);
  }
  return arr
}

console.log('返回到数组:', setArray<string>(3,'zy')) // ['zy', 'zy','zy']
console.log('返回到数组:', setArray<number>(3, 1)) // [1,1,1]

多个类型参数

在一些时候我们需要定义多个类型占位符,我们可以使用多个字母来代替

const swap = <T, U>(value: [T,U]): [U, T] => {
  return [value[1], value[0]]
}

console.log('swap', swap<string, number>(['weinan', 1000])) // swap [ 1000, 'weinan' ]

泛型约束

在函数内使用泛型的时候,因为不明确值的类型,有些值的属性在定义的时候会报错,比如下面的例子

const getLength = <T>(val:T) => {
  return `${val}长度:${val.length}` // Error 因为泛型 T 不一定包含属性 length,所以编译的时候报错了
}

这个时候为了解决这个问题,我们可以对泛型约束,使其只能传入那些包含length 属性的变量,这就是泛型约束

interface Attribute {
  length: number;
}
const getLength = <T extends Attribute>(val:T) => {
  return `${val}长度:${val.length}`
}
getLength('hello world')
getLength(180) // Error 报错 number类型的值是没有length属性 所以就会报错

注意我们在泛型里面使用的extends表示泛型约束,需要跟接口的继承区分开

泛型接口

定义接口的时候也可以使用泛型

interface Cat<T> {
  list: Array<T>;
}

let cat:Cat<{name:string, age:number}> = {
  list: [{name: '三花', age: 2}]
}
console.log('cat:', cat); // cat: { list: [ { name: '三花', age: 2 } ] }

泛型类

定义类的时候也是可以使用泛型的

class MyArray<T> {
  private list: Array<T> = [];
  add(name: T) {
    this.list.push(name)
  }
  getMax(): T {
    let result = this.list[0];
    for (let i = 0; i < this.list.length; i++) {
      if(result < this.list[i]) result = this.list[i];
    }
    return result
  }
}

let myArray = new MyArray()
myArray.add(1);
myArray.add(2);
myArray.add(3);
console.log('最大值', myArray.getMax()) // 最大值 3

泛型类型别名

类型别名使用泛型如下:

type typeAlias<T> = {list:T[]} | T[];
let array:typeAlias<string> = {list: ['zy']}
let array1:typeAlias<number> = [1,2,4]
console.log(array); // { list: [ 'zy' ] }
console.log(array1) // [ 1, 2, 4 ]

泛型的默认类型

我们可以为泛型中的类型参数指定默认类型,当泛型使用的时候没有声明的时候,默认类型就生效了

const createArray = <T = string>(length: number, value: T): Array<T>  =>{
  let result: T[] = [];
  for (let i = 0; i < length; i++) {
    result[i] = value;
  }
  return result;
}

console.log(createArray(3,'str')) // [ 'str', 'str', 'str' ]

关键词

typeof 关键词

在 javaScript 中 typeof 可以判断一个变量的基础数据类型,在TS中,它还可以用来获取一个变量的声明类型


const obj = { a: '1' };
type Foo = typeof obj;  // type Foo = { a: string }

keyof 关键词

keyof 可以获取一个对象接口的所有 key 值

interface Obj {
  name: string,
  age: number
}

type Foo = keyof Obj; // type Foo = 'name' | 'age'
let list: Foo = 'name'

索引访问操作符

使用 [] 操作符可以进行索引访问

interface Person {
  name: string;
  age: number;
}

type x = Person["name"]; // type x = string

映射类型 in

在定义的时候可以用 in 操作符去批量定义类型中的属性

interface Person {
  name: string;
  age: number;
  gender: "male" | "female";
}
//批量把一个接口中的属性都变成可选的
type PartPerson = {
  [Key in keyof Person]?: Person[Key];
};

let p1: PartPerson = {};

// in 遍历 Keys,并为每个值赋予 string 类型
type Keys = 'a' | 'b' | 'c'
type Obj = {
  [T in Keys]: string
}

内置工具类型

Exclude

Exclude<T,U> 表示从T可分配的类型里面排除U

// 源码
// 如果 T 中的类型在 U 不存在,则返回,否则抛弃。(ps: lib.es5.d.ts(1506, 6) 已声明)
type Exclude<T, U> = T extends U ? never : T; 
type E = Exclude<string | number, string>;
let e: E = 'zy'; // 报错
let e: E = 10;

用法升级

interface Worker {
  name: string
  age: number
  email: string
  salary: number
}

interface Student {
  name: string
  age: number
  email: string
  grade: number
}

// 用Exclude来完成的排除Student接口中跟 Worker接口重复的key值
type E = Exclude<keyof Student, keyof Worker>
let e:E = 'grade' // true
let e1:E = 'name' // 报错: 不能将类型“"name"”分配给类型“"grade"”

Extract

Extract<T,U> 表示从T可分配的类型里面提取U,常用来提取公共实例

// 源码
// 如果 T 中的类型在 U 不存在,则返回,否则抛弃。
type Extract<T, U> = T extends U ? T : never;
type E = Extract<string | number, number>
let e: E = "1";
interface ITeacher {
  age: number;
  gender: string;

}

interface IStudent {
  age: number;
  gender: string;
  homeWork: string;
}

type CommonKeys = Extract<keyof ITeacher, keyof IStudent>; // "age" | "gender"

Record

Record<K,T> 构造一个类型,该类型具有一组属性 K,每个属性的类型为 T。可用于将一个类型的属性映射为另一个类型。Record 后面的泛型就是对象键和值的类型 简单理解:K 对应对象的 key,T 对应对象的 value,返回的就是一个声明好的对象

//源码 
type Record<K extends keyof any, T> = {
  [P in K]: T;
};

enum Methods {
  GET = "get",
  POST = "post",
}
type IRouter = Record<Methods, (req: any, res: any) => void>;

let request:IRouter = {
  get: function (req: any, res: any) {},
  post: function (req: any, res: any) {}
}

NonNullable

NonNullable 从T排除 null 和 undefind

// 源码
type NonNullable<T> = T extends null | undefined ? never : T;

type E = NonNullable<string | number | null | undefined>;
let e: E = undefined; // 报错

ReturnType

ReturnType 表示在 extends 条件语句中待推断的类型变量, 常用来获取函数返回值类型

// 源码
type ReturnType<T extends (...args: any[]) => any> = T extends (
  ...args: any[]
) => infer R
  ? R
  : any;

const getUserInfo = () => {
  return { name: "hello", age: 10 };
}

// 通过 ReturnType 将 getUserInfo 的返回值类型赋给了 UserInfo
type UserInfo = ReturnType<typeof getUserInfo>
const userInfo: UserInfo = {
  name: 'zy',
  age: 1000
}

Parameters

Parameters的作用是用来获取一个函数的参数类型,而且返回的是只能包含一组类型的数组

// 源码
type Parameters<T> = T extends (...args: infer R) => any ? R : any;

type T0 = Parameters<() => string>; // []
type T1 = Parameters<(s: string) => void>; // [string]
type T2 = Parameters<<T>(arg: T) => T>; // [unknown]

Partial

Partial 可以把传入的属性由非可选变为可选

// 源码
type Partial<T> = { [P in keyof T]?: T[P] };

interface A {
  a1: string;
  a2: number;
  a3: boolean;
}
type aPartial = Partial<A>;
const a: aPartial = {}; // 不会报错,因为属性现在已经全部变成可选了

Required

Required 可以把传入的属性从可选属性变成非可选属性

interface Person {
  name: string;
  age: number;
  gender?: "male" | "female";
}
type UserInfo = Required<Person>
let userInfo: UserInfo = { // 报错,因为现在gender为非可选属性
  name: 'John',
  age: 1
}

Readonly

Readonly 可以将传入的属性每一项都加上readonly, 变成只读属性

// 源码
type Readonly<T> = { readonly [P in keyof T]: T[P] };

interface Person {
  name: string;
  age: number;
  gender?: "male" | "female";
}

let p: Readonly<Person> = {
  name: "hello",
  age: 10,
  gender: "male",
};
p.age = 11; //Error 因为Person的属性全部变成只读属性了,不能更改

Pick

Pick<T, U> 可以用传入的类型T上,筛选出想要的全部或极个别的属性和类型 (ps: U的值必须要在T内的属性,U的值如果为多个用|隔开)

// 源码
type Pick<T, K extends keyof T> = { [P in K]: T[P] };

interface Todo {
  title: string;
  description: string;
  done: boolean;
}
type TodoBase = Pick<Todo, "title" | "done">;
let todoBase: TodoBase = {
  title: 'hzx',
  done: false,
  description: '', // Error 因为定义的类型TodoBase筛选的时候过滤了TodoBase
}

Omit

Omit<T,U> 从传入的类型T中剔除类型U,并返回新类型

type User = {
id: string;
name: string;
email: string;
};
type UserWithoutEmail = Omit<User, "email">;// UserWithoutEmail ={id: string;name: string;}
};

装饰器

装饰器是一种特殊类型的声明,它能够被附加到类声明、方法、属性或参数上,可以修改类的行为,装饰器使用 @expression 这种形式,expression 求值后必须为一个函数,它会在运行时被调用,被装饰的声明信息做为参数传入。 常见的装饰器有类装饰器、属性装饰器、方法装饰器和参数装饰器 装饰器的写法分为普通装饰器和装饰器工厂 使用@装饰器的写法需要把 tsconfig.json 的 experimentalDecorators 字段设置为 true

类修饰器

类装饰器在类声明之前声明,用来监视、修改或替换类定义 普通类修武器

// namespace 是命名空间 后面会提及
namespace A {
  //当装饰器作为修饰类的时候,会把构造器传递进去
  function addNameEat(constructor: Function){
    constructor.prototype.name = "小明";
    constructor.prototype.eat = function(){
      console.log('eat');
    }
  }
  // 这里的修饰器 修改了类 Person 的 name属性 和 eat属性
  @addNameEat
  class Person {
    name!: string;
    eat!: Function;
    constructor() {}
  }
  let p: Person = new Person();
  console.log('name', p.name) // name 小明
  p.eat() // eat
}

修饰器工厂 如果你需要让修饰器可以携带参数的话,可以使用修饰器工厂

namespace B {
  function addNameEat(name: string) {
    return function(constructor: Function){
      constructor.prototype.name = name;
      constructor.prototype.eat = function(){
        console.log('eat');
      }
    }
  }
  @addNameEat('小亮')
  class Person {
    name!: string;
    eat!: Function;
    constructor() {};
  }
  let p: Person = new Person();
  console.log('name', p.name) // name 小亮
  p.eat() // eat
}

替换类 修饰器还可以用来替换类,但是替换的类的结构要和原来的类的结构一致

namespace C {
  function addNameEat(constructor: Function) {
    return class {
      name: string = '小红';
      eat() {
        console.log('eat')
      }
    }
  }
  @addNameEat // error, 因为要替换的类和原来的类的结构不一致
  class Person {
    name !: string;
    str !: string;
    eat() {
      console.log('eat-1')
    };
    constructor() {};
  }
  let p: Person = new Person();
  console.log('name', p.name)
  p.eat()
}

属性装饰器

属性装饰器表达式会在运行时当作函数被调用,传入 2 个参数 第一个参数对于静态成员来说是类的构造函数,对于实例成员是类的原型对象 第二个参数是属性的名称

namespace D {
  function addNameEat(params: string) {
    return function (target: any, attrs: any) {
      console.log('target',target)
      console.log('attrs',attrs)
      target[attrs] = params
    }
  }
  class Person {
    @addNameEat('http://localhost:8080')
    apiUrl !:string;
    constructor() {}
  }
  let p: Person = new Person();
  console.log('apiUrl:', p.apiUrl); // apiUrl: http://localhost:8080
}

方法装饰器

方法装饰器被应用到方法的属性描述上,可以用来监视、修改或者替换方法定义。 它接收三个参数: 1、target: Object - 对于静态成员来说是类的构造函数,对于实例成员是类的原型对象。 2、 propertyKey: string | symbol - 方法名。 3、 descriptor: PropertyDescriptor - 成员的属性描述符 具体属性如下:

({
 value: any,
 writable: boolean, 
 enumerable: boolean, // 对象属性的可枚举性
 configurable: boolean
})。

ps: 注意如果代码输出目标版本小于ES5,属性描述符 将会是undefined

修饰实例方法

namespace E {
  // 修饰实例方法
  function noEnumerable(target: any, property: string, descriptor: PropertyDescriptor) {
    console.log('target',target)
    console.log('property', property)
    descriptor.enumerable = false // 只返回可枚举的属性
  }
  class Person {
    constructor(public name:string) {}
    @noEnumerable
    getName() {
      return this.name
    }
  }
  const p: Person = new Person('hello world');
  // 如果直接打印,该对象中不包含 getName() 方法,因为该方法是不可枚举的。
  console.log('name',p) // name Person { name: 'hello world' }
  // 但是可以调用该方法
  console.log('name:',p.getName()) // name: hello world
}

重写方法

namespace F {
//重写方法
function toNumber(
  target: any,
  property: string,
  descriptor: PropertyDescriptor
) {
  let oldMethod = descriptor.value;
  descriptor.value = function (...args: any[]) {
    args = args.map((item) => parseFloat(item));
    return oldMethod.apply(this, args);
  };
}
class Person {
  name: string = "hello";
  public static age: number = 10;
  constructor() {}
  getName() {
    console.log(this.name);
  }
  @toNumber
  sum(...args: any[]) {
    return args.reduce((accu: number, item: number) => accu + item, 0);
  }
}
let p: Person = new Person();
p.getName();
console.log(p.sum("1", "2", "3"));
}

修饰器工厂

// 装饰器工厂
function enumerable(bool: boolean) {
  return function (
    target: any,
    propertyName: string,
    descriptor: PropertyDescriptor,
  ) {
    return {
      value: function () {
        return 'Error: name is undefined'
      },
      enumerable: bool,
    }
  }
}
class Info {
  constructor(public name: string) {}
  @enumerable(false)
  getName() {
    return this.name
  }
}
const info = new Info('一库')
console.log(info) // { name: '一库' }
console.log(info.getName()) // Error: name is undefined

参数修饰器

参数装饰器顾名思义,是用来装饰函数参数,它接收三个参数: 1、target: Object - 被装饰的类 2、propertyKey: string | symbol - 方法名 3、parameterIndex: number - 方法中参数的索引值 参数装饰器的作用是用于监视一个方法的参数是否被传入,参数装饰器的返回值会被忽略。示例代码如下:

namespace J {
  function required(target: any, propertyName: string, index: number) {
    console.log(`修饰的是${propertyName}的第${index + 1}个参数`)
  }
  class Info {
    name: string = '小明'
    age: number = 18
    getInfo(prefix: string, @required infoType: string): string {
      return prefix + ' ' + this[infoType]
    }
  }
  interface Info {
    [key: string]: string | number | Function
  }
  const info = new Info()
  info.getInfo('', 'age') // 修饰的是getInfo的第2个参数
}

修饰器的执行顺序

  1. 有多个参数装饰器时:从最后一个参数依次向前执行
  2. 方法和方法参数中参数装饰器先执行
  3. 方法和属性装饰器,谁在前面谁先执行。因为参数属于方法一部分,所以参数会一直紧紧挨着方法执行
  4. 类装饰器总是最后执行

tsconfig.json配置

重要字段

  • files - 设置要编译的文件的名称;
  • include - 设置需要进行编译的文件,支持路径模式匹配;
  • exclude - 设置无需进行编译的文件,支持路径模式匹配,默认node_module;
  • compilerOptions - 设置与编译流程相关的选项。

compilerOptions 选项

{
  "compilerOptions": {

    /* 基本选项 */
    "target": "es2016",                       // 指定 ECMAScript 目标版本: 'ES3' (default), 'ES5', 'ES6'/'ES2015', 'ES2016', 'ES2017', or 'ESNEXT'
    "module": "commonjs",                  // 指定使用模块: 'commonjs', 'amd', 'system', 'umd' or 'es2015'
    "lib": ["ES2015"],                             // 指定要包含在编译中的库文件
    "allowJs": true,                       // 允许编译 javascript 文件
    "checkJs": true,                       // 报告 javascript 文件中的错误
    "jsx": "preserve",                     // 指定 jsx 代码的生成: 'preserve', 'react-native', or 'react'
    "declaration": true,                   // 生成相应的 '.d.ts' 文件
    "sourceMap": true,                     // 生成相应的 '.map' 文件
    "outFile": "./",                       // 将输出文件合并为一个文件
    "outDir": "./",                        // 指定输出目录
    "rootDir": "./",                       // 用来控制输出目录结构 --outDir.
    "removeComments": true,                // 删除编译后的所有的注释
    "noEmit": true,                        // 不生成输出文件
    "importHelpers": true,                 // 从 tslib 导入辅助工具函数
    "isolatedModules": true,               // 将每个文件做为单独的模块 (与 'ts.transpileModule' 类似).

    /* 严格的类型检查选项 */
    "strict": true,                        // 启用所有严格类型检查选项
    "noImplicitAny": true,                 // 在表达式和声明上有隐含的 any类型时报错
    "strictNullChecks": true,              // 启用严格的 null 检查
    "noImplicitThis": true,                // 当 this 表达式值为 any 类型的时候,生成一个错误
    "alwaysStrict": true,                  // 以严格模式检查每个模块,并在每个文件里加入 'use strict'

    /* 额外的检查 */
    "noUnusedLocals": true,                // 有未使用的变量时,抛出错误
    "noUnusedParameters": true,            // 有未使用的参数时,抛出错误
    "noImplicitReturns": true,             // 并不是所有函数里的代码都有返回值时,抛出错误
    "noFallthroughCasesInSwitch": true,    // 报告 switch 语句的 fallthrough 错误。(即,不允许 switch 的 case 语句贯穿)

    /* 模块解析选项 */
    "moduleResolution": "node",            // 选择模块解析策略: 'node' (Node.js) or 'classic' (TypeScript pre-1.6)
    "baseUrl": "./",                       // 用于解析非相对模块名称的基目录
    "paths": {},                           // 模块名到基于 baseUrl 的路径映射的列表
    "rootDirs": [],                        // 根文件夹列表,其组合内容表示项目运行时的结构内容
    "typeRoots": [],                       // 包含类型声明的文件列表
    "types": [],                           // 需要包含的类型声明文件名列表
    "allowSyntheticDefaultImports": true,  // 允许从没有设置默认导出的模块中默认导入。

    /* Source Map Options */
    "sourceRoot": "./",                    // 指定调试器应该找到 TypeScript 文件而不是源文件的位置
    "mapRoot": "./",                       // 指定调试器应该找到映射文件而不是生成文件的位置
    "inlineSourceMap": true,               // 生成单个 soucemaps 文件,而不是将 sourcemaps 生成不同的文件
    "inlineSources": true,                 // 将代码与 sourcemaps 生成到一个文件中,要求同时设置了 --inlineSourceMap 或 --sourceMap 属性

    /* 其他选项 */
    "experimentalDecorators": true,        // 启用装饰器
    "emitDecoratorMetadata": true          // 为装饰器提供元数据的支持
  }
}

模块和声明文件

全局模块

在默认情况下,当你开始在一个新的 TypeScript 文件中写下代码时,它处于全局命名空间中 使用全局变量空间是危险的,因为它会与文件内的代码命名冲突,推荐使用文件模块

// foo.ts
const foo = 123;
// bar.ts
const bar = foo; // 这里是可以获取到值的,因为foo现在是个全局变量

文件模块

  • 文件模块也被称为外部模块。如果在你的 TypeScript 文件的根级别位置含有 import 或者 export,那么它会在这个文件中创建一个本地的作用域
  • 模块是 TS 中外部模块的简称,侧重于代码和复用
  • 模块在其自身的作用域里执行,而不是在全局作用域里,可以避免变量重复问题
  • 一个模块里的变量、函数、类等在外部是不可见的,除非你把它导出
  • 如果想要使用一个模块里导出的变量,则需要导入
// foo.ts
const foo:number = 123;
export {}
// bar.ts
const bar:number = foo; // Error 因为foo 现在不是全局变量了

声明文件

基于 Typescript 开发的时候,很麻烦的一个问题就是类型定义。导致在编译的时候,经常会看到一连串的找不到类型的提示。“d.ts”文件用于为 TypeScript 提供有关用 JavaScript 编写的 API 的类型信息。简单讲,就是你可以在 ts 中调用的 js 的声明文件。TS的核心在于静态类型,我们在编写 TS 的时候会定义很多的类型,但是主流的库都是 JS编写的,并不支持类型系统。这个时候你不能用TS重写主流的库,这个时候我们只需要编写仅包含类型注释的 d.ts 文件,然后从您的 TS 代码中,可以在仍然使用纯 JS 库的同时,获得静态类型检查的 TS 优势,在 Typescript 2.0 之后,推荐使用 @types 方式来按照声明文件,例如: ​

在TS中使用cdn的方式引入jquery的时候,这个时候使用jquery的时候因为tsc不清楚jquery是啥,所以会报错

jQuery('#id') // 找不到名称“jQuery”。

使用 declare 关键字告诉 tsc,表示 jQuery 变量已经在其他位置定义(ps: 使用 declare 关键字时,我们不需要编写声明的变量、函数、类的具体实现(因为变量、函数、类在其他库中已经实现了),只需要声明其类型即可,否则会报错)

// 注意声明文件里面只能声明类型
declare var jQuery: (selector: string) => any;
jQuery('#id') // true

一般将声明放入 .d.ts 文件,表示该文件有适配 ts 的类型声明:

// jquery.d.ts
declare var jQuery: (selector: string) => any;

注意如果是自定义的声明文件,因为typeScript2.0 之后将会默认查看 ./node_modules/@types 文件夹,自动从这个文件夹获取模块的类型定义,因此需要在 tsconfig.json 里面配置 typeRoots

//比如我的.d.ts 文件写在typings文件夹下
{
  "compilerOptions": {
    "typeRoots": ["./typings"]
  }
}

ps: 注意如果你安装了@type 的话,但是你同时又存在本地声明文件 那么 typeRoots 的值应该为 typeRoots: ['./typeings', './node_modules/@types']

namespace 命名空间

不同于声明模块,命名空间一般用来表示具有很多子属性或者方法的全局对象变量。 我们可以将声明命名空间简单看作是声明一个更复杂的变量。

declare namespace $ {
  const version: number;
  function ajax(settings?: any): void;
}
$.version; // => number
$.ajax();

目前市面上常见的包,比如 JQuery、React 这种都有现成的声明文件(.d.ts),都是由社区贡献的(直接 npm i @types/* 即可) 可以在以下网址搜索第三方相应的声明文件: @types 搜索声明库 @types 官方声明文件库