TypeScript 入门

116 阅读15分钟

TypeScript 入门文档

TS介绍

TS是一种开源的编程语言,是JavaScript的一个超集。TypeScript添加了静态类型和其他一些特性,以提供更强大的类型系统和工具,并使我们能够更好地进行大型应用程序的开发。

TypeScript的特点包括:

  1. 静态类型检查:TypeScript引入了静态类型系统,允许在编码阶段捕获常见错误,并提供更好的代码补全和智能提示。它使用类型注解和类型推导来帮助开发者在编写代码时进行类型检查。
  2. 类和接口:TypeScript支持面向对象编程的概念,包括类、接口、继承、多态等,使得代码结构更清晰、可维护性更好。
  3. ES6+特性支持:TypeScript完全兼容ES6+的语法和功能,可以享受到JavaScript最新版本带来的优势,如箭头函数、模块化、解构赋值、异步/await等。
  4. 编译时类型检查:TypeScript代码在开发阶段会进行编译,将TypeScript代码转换为纯粹的JavaScript代码,并在此过程中进行类型检查,确保类型的准确性。
  5. 工具支持:TypeScript拥有强大的工具生态系统,如编辑器(如VS Code)的智能感知、代码补全和重构等功能,还支持各种构建工具(如Webpack、Rollup等)和调试器。

总的来说,TypeScript为JavaScript提供了更强大的类型系统、更好的开发工具支持以及更丰富的语言特性,使得开发者能够编写出更健壮、可维护的应用程序。

环境配置

  1. 创建文件夹,并且进行初始化:

    npm init

  2. 安装typescript

    npm i typescript,将ts包安装在当前目录中

  3. typescript初始化:

    npx tsc --init 执行完毕后会在本地目录创建出tsconfig.json文件

  4. 修改tsconfig.json配置项:

    "rootDir": "./src" "outDir": "./dist"

  5. 创建distsrc目录,执行命令:

    npx tsc --w启动项目,监听代码

  6. 在src下编写ts代码,在dist目录查看编译之后的代码

介绍

TypeScript, JS的超集。其中TS百分百支持所有的JS的语法,并且对于JS正处于草案尚不支持的语法,TS也能够做到支持。

类型标注

在ts中,可以通过: 类型的形式来对变量进行类型标注。

let a: number;

当类型标注成功之后,就只能用当前变量存储标注类型对应的数据。

类型推断

在ts文件中,就算是没有直接给变量进行类型标注,但是当我们对变量进行第一次赋值的时候,ts就会进行自动的类型推断,当自动类型推断完成,后续的变量赋值也必须符合类型推断的标注。

image-20230724144845482

上面的代码中,变量b就被推断成了布尔类型的变量,那么后续的赋值就只能赋值为布尔类型。

联合类型

TS中,如果想要给一个变量标注为多种类型可能性,那么可以使用联合类型。

例如:

// 联合类型
let strAndNum: string | number

上面代码中的变量strAndNum此时被标注为字符串或者number类型,通过|实现了一个联合类型。所以在给变量赋值的时候,字符串或者数字类型的数据都是符合要求的。

数据类型

TS中对JS的所有数据类型都是支持的。

基础数据类型

例如:

// ts中对js基础类型的支持
let s: string = 'hello,ts'
let b: boolean = true
let n: number = 100
let nu: null = null
let u: undefined = undefined

上面的示例演示的是基础类型的标注,下面来讲解ts引用数据类型进行标注。

数组

如果想要给一个数组进行标注,可以通过下面的形式进行:

  • 类型[]
  • Array<类型>

第二种方式属于泛型的应用,后面讲解到泛型的时候会重点讲解。

示例:

// 对数组类型数据进行标注
let arr: number[] = [1,2,3,4,5]
let arr2: Array<string> = ['a', 'b', 'c', 'd']

函数

对函数的类型标注主要集中在函数的形参和返回值上面。

比如:

// 对函数进行类型标注
function fn1(par1: string): string {
    return par1 + '!'
}

如果函数没有返回值,可以将返回值标注为void。

比如:

function fn2(): void {
    console.log('该函数没有返回值')
}

除了这种函数的定义形式外,还可以在函数表达式上进行参数和返回值的类型标注。

比如:

let fn3 = (par1: string): void => {
    console.log('par1的参数值为:' + par1)
}

需要注意,此时我们对于函数的标注是在函数体本身进行的标注,当把函数赋值给变量时,我们并没有对变量进行直接的标注。

如果把鼠标放到变量名的身上,就可以发现,此时变量名身上的标注其实是自动推断而来的标注。

image-20230725053416830

我们可以模仿ts自动推断得出的结果这种写法,来显示的对变量进行类型标注。

let fn4: (par1: string) => void = (par1: string): void => {
    console.log('par1的参数值为:' + par1)
}

这个时候在代码中就出现了两个箭头,稍显冗余也比较容易出错,这里建议可以使用type类型别名来进行处理。

object

关于对象的类型标注,主要集中在对象中的属性和方法的身上。

// 对象的标注
  const obj: {
    a: number,
    b: number,
    add: (p1: number, p2: number) => number
  } = {
    a: 100,
    b: 200,
    add(p1: number, p2: number): number{
      if (!p1 || !p2) {
        return this.a + this.b 
      } 
      return p1 + p2
    }
  }

TS中的数据类型

字面量类型

当直接使用一个具体的字面量值作为变量的类型标注时,那么这个变量的类型就属于字面量类型,赋值的时候也只能被赋值为和标注的字面量相同的值。

  // 字面量
  let a: 100 // 此时变量a 就被标注为字面量100,以100 作为类型,赋值的时候只能赋值100
  a = 100

如果赋值为其他的值,则会抛出异常。

image-20230725080816179

一般来说,字面量类型的标注都会和联合类型配合使用:

  let b: 100 | 200 
  // 此时b的值只能是100 或者200
  b = 200

当给一个常量直接赋值而没有进行类型标注的时候,那么常量的类型就会被自动标注为字面量类型,字面量的值就是初始化的值。

any

any在ts中,表示任意类型。当一个变量被标注为any时,那么该变量就可以接受任意类型的参数。

  // any类型
  let a: any = 100
  a = true 
  a = 'hello,world'
  a = null
  a = undefined

虽然看上去any很好用,不会受到类型的限制,但是还是建议尽量少 的去使用any,否则TypeScript就变成了AnyScript。

unknown

unknown 类型在ts中表示为未知类型,可以理解为不知道是什么类型的类型。

// unknown
let b: unknown = true

标注为unknown的类型,可以赋值为其他类型的数据。

b = 100
b = 'hello'
b = null 
b = undefined

b = []
b = {} 
b = () => {}

不过unknown类型的数据无法赋值给其他类型的数据。

  let c: string 
  c = b

会出现如下的异常提示:

image-20230725065843371

never

never一般使用较少,表示永不存在的值。较为常见的用法是用在标注一个函数抛出错误时对返回值进行标注。

比如:

  let fn1 = (): never => {
    throw new Error('123')
  }

tuple

tuple 可以理解为元组。它和数组很像,但是区别在于,数组中的长度不固定,每个索引位存储的数据类型不确定,而元组正好与其相反。

比如:

  // 创建一个元组类型的数据
  let list1: [string, number] = ['hello', 100]
  // 如果初始化的数据和约束的元组类型不同,则会抛出异常
  list1 = [100, 'abc']

第二次赋值引发的异常如下:

image-20230725071248024

在做元组类型标注的时候,元组中设置了几个数据类型,那么也就意味着元组中的长度为几。

比如,上面标注的元组为[string, number],那么此时这个元组的长度就为2。

如果传入多余标注的长度,那么ts就会抛出异常。

枚举

通过枚举类型,可以给几个常量分配不同的值。

比如:

  // 枚举类型
  enum Male {
    man,
    woman
  }

  console.log(Male.man) // 0
  console.log(Male.woman) // 1

在上面的代码中,我们并没有手动显示的给Male中的变量设置值,但是当打印的时候,可以看到结果为0和1。这也就表示此时 ts自动给程序分配了不同的值。

也可以在创建枚举的时候,自己手动赋值,而不是依赖于ts。

  enum Num {
    a = 2,
    b = 4,
    c = 6,
    d = 8
  }

  console.log(Num.a, Num.b, Num.c, Num.d) // 2 4 6 8 

如果在赋值的过程中仅仅给某个变量赋值,那么会出现下面的情况:

  enum Num {
    a = 2,
    b,
    c,
    d
  }
  console.log(Num.a, Num.b, Num.c, Num.d) // 2 3 4 5

可以在上面的代码中看到,当把a显示的赋值为2之后,后续的b、c、d都自动进行了基于a的递增。

如果从中间开始赋值,那么递增就会从中间开始:

  enum Num {
    a,
    b,
    c = 100,
    d
  }
  console.log(Num.a, Num.b, Num.c, Num.d) // 0 1 100 101

上面代码中,a的值是0, b的值是1, c显示赋值为100,那么d的值就为递增之后的101

类型别名

在ts中,可以把较为复杂的类型使用type进行重新命名,从而简化标注。

比如:

  // 类型别名
  let add: (num1: number, num2: number) => number = (num1: number, num2: number): number => {
    return num1 + num2;
  }

例如上面的代码,在add变量名后面做的类型标注较为冗长,不太利于阅读和重复使用,这个时候就可以通过type来设置一个类型别名。

比如:

  // 类型别名
  type addFuncType = (num1: number, num2: number) => number

  let add: addFuncType = (num1: number, num2: number): number => {
    return num1 + num2;
  }

常见的像对象的标注也可以使用类型别名进行定义:

type userInfoType = {
    user: string,
    age: number 
  }

  const userInfo: userInfoType = {
    user: 'zs',
    age: 30
  }

其他的数据类型也可以使用type进行定义,从而让程序变得更加利于阅读。

type的继承

在定义type的时候,如果想要将多个type的标注合并,可以使用&

  // type的继承
  type userInfo = {
    user: string,
    age: number
  }

  type studentInfo = {
    grade: string
  } & userInfo

  let user: studentInfo = {
    user: 'zs',
    age: 30,
    grade: '三年二班'
  }

这种通过&实现的类似于继承的写法,和下面的交集的写法产生的标注效果类似。

交集

可以通过&来实现交集的设置。

type userType = {
    user: string,
    age: number
  }
  type studentType = {
    user: string, 
    grade: string 
  }

  const user: userType & studentType = {
    user: 'zs',
    age: 30,
    grade: '三年二班'
  }

设置交集的时候,两个type中相同的字段如果类型不同,那么就会被ts理解为是never。

比如:

image-20230725075739456

image-20230725075747927

user字段被程序理解为never。

并集

也可以通过| 来让多个type形成并集的关系。

type s = string 
type n = number 
const a: s | n = 100 // a 的值可以是string 或者number

interface

interface 被ts定义为接口,主要用来标注对象类型 。

interface Person {
    user: string,
    age: number
  }

  const user: Person = {
    user: 'zs',
    age: 30
  }

interface 和type的区别就在于interface只能用来标注约束对象。同时interface的继承和type也有区别:

  interface Person {
    user: string,
    age: number
  }

  interface Student extends Person {
    grade: string 
  }

  const user: Student = {
    user: 'zs',
    age: 30,
    grade: '三年二班'
  }

类型断言

这里的类型断言不同于上面的类型推断。类型推断指的是在不进行标注的情况下,ts会根据赋的值进行自动的标注。

而这里的类型断言指的是手动的进行判断,告诉ts,值的类型是什么。

当一个变量的类型可能是多种类型的时候,就会出现在调用方法时的报错。比如:

  function sayHello(param: number|string) {
    console.log(param.includes('hello'))
  }

上面的代码程序就会出现异常:

image

出现异常的原因就在于ts不知道param的类型到底是number还是string,自然当你调用string类型数据身上方法的时候就会报错。

这个时候可以使用类型断言来解决问题。

  function sayHello(param: number|string) {
    console.log((param as string).includes('hello'))
  }

param as string 相当于是告诉ts,我已经确定,当前形参param必然是一个字符串,你可以放心的调用includes方法。

类型断言可能导致ts判断失误,所以还是尽量少用。

泛型

通过泛型,可以动态的设置类型参数。比如定义一个函数的时候,参数的类型并不确定,那么可以让用户在调用函数的时候传入具体参数类型即可。

  function fn1<T>(param:T) {
    console.log(param)
  }

  fn1<number>(100)

也可以在定义对象的时候使用泛型。

  type objType<T> = {
    user: string,
    info: T
  } 

  const obj: objType<string> = {
    user: 'zs',
    info: 'hello,world'
  }

当使用泛型时,可以通过extends实现继承:

  function  getLength<T extends string>(args: T) {
    return args.length
  }

  getLength('hello,world')

当然除了继承这些JS固有类型以外,还可以继承一个自定义的对象。

  type obj = {
    length: number,
  }
  function getLength<T extends obj>(arg: T):number {
    return arg.length;
  }

如果想要让泛型拥有更多的约束,可以使用extends来继承其他类型,从而添加约束规则。

Omit

Omit 类型接受两个参数:第一个参数是要从中排除属性的类型,第二个参数是要排除的属性的名称或联合类型。

type Person = {
    name: string
    age: number
}

type Son = Omit<Person, 'age'>

// Son === { name: string }

Pick

Pick 类型接受两个参数:第一个参数是要从中选择属性的类型,第二个参数是要选择的属性的名称或联合类型。

type Person = {
    name: string
    age: number
}
type Son = Omit<Person, 'age'>

// Son === { age: number }

Partial (将指定的类型所有的属性都设置成可选)

在TypeScript中,Partial是一个内置的泛型类型,它可以用来将指定类型T的所有属性都设置为可选的。 具体来说,Partial可以将一个类型的所有属性转换为可选属性,使得在使用该类型时可以只传递部分属性或者不传递任何属性。这在编写可选参数的函数、构建复杂对象或者进行对已有对象的修改时非常有用。

interface Person {
  name: string;
  age: number;
  address: string;
}
type PartialConsult = Partial<Person>
// PartialConsult就变成这样样子了:
// interface PartialConsult {
//   name?: string;
//   age?: number;
//   address?: string;
// }

Required(将指定的类型所有的属性都设置成必选)

与Partial类似,可以指定的类型所有属性都设置为必选的

装饰器(语法糖)

装饰器是一种特殊类型的声明,它能够被附加到类声明方法属性或者参数上,

  • 语法:装饰器使用 @expression 这种形式,expression求值后必须为一个函数,它会在运行时被调用,被装饰的声明信息做为参数传入
  • 若要启用实验性的装饰器特性,必须tsconfig.json里启用experimentalDecoratorsemitDecoratorMetadata编译器选项
  • 常见的装饰器有: 类装饰器属性装饰器方法装饰器参数装饰器
  • 装饰器的写法: 分为普通装饰器(无法传参)和`装饰器工厂(可以传参)

装饰器的基本使用

例如:

// 方式1
function personEnhancer(target: any) {
    target.prototype.hello = () => {
      console.log("hello,world");
    };
}

// 方式2 
const personEnhancer: ClassDecorator = (target: Function) => {
    // target 表示使用装饰器的类
    target.prototype.hello = function() {
      console.log('hello,world')
    }
}

@personEnhancer
class Person {
	constructor() {
}

public hello() {}
}

const p1 = new Person();
p1.hello()

装饰器的本质其实相当于是将类当作参数传递到了一个函数当中。

如下:

const personEnhancer: ClassDecorator = (target: Function) => {
    // target 表示使用装饰器的类
    target.prototype.hello = function() {
      console.log('hello,world')
    }
  }

  @personEnhancer
  class Person {
    constructor() {
    }
    
    public hello() {}
  }

  // 装饰器的本质
  const p = new Person() 
  personEnhancer(Person) 
  p.hello()

如果在使用的时候,一个装饰器并不能解决问题,那么可以同时使用多个装饰器。

const user: ClassDecorator = (target: Function) => {
    console.log('user')
    target.prototype.sayHello = () => {
      console.log('这是装饰器user更新的方法--sayHello')
    }
  }

  const info: ClassDecorator = (target: Function) => {
    console.log('info')
    target.prototype.show = function() {
      console.log('这是装饰器info更新的方法--show')
      this.sayHello() // 在装饰器的方法中可以设置通过this访问实例化对象中的内容
    }
  }

  @user
  @info // 会先执行info,在执行user,从下到上执行装饰器
  class Person {
    constructor() {
    }
    sayHello() {}
    show() {}
  }

  const p = new Person()
  p.sayHello()
  p.show()

装饰器工厂

在使用装饰器的时候,可以传递参数,装饰器可以根据参数作出不一样的应对。这种写法就是装饰器工厂。

例如:

const MusicDecoratorFactory = (type: string): ClassDecorator => {
    switch (type) {
        case 'Tank':
            return (target: Function) => {
                target.prototype.playMusic = (): void => {
                    console.log('播放战争音乐');
                };
            };

        default:
            return (target: Function) => {
                target.prototype.playMusic = (): void => {
                    console.log('播放玩家音乐');
                };
            };
    }
};
@MusicDecoratorFactory('Tank')
class Tank {
    public playMusic() {}
    public run() {
        this.playMusic();
    }
}
@MusicDecoratorFactory('Player')
class Player {
    public playMusic() {}
    public run() {
      this.playMusic();
    }
}

const p = new Player();
p.run();
const t = new Tank();
t.playMusic();

方法装饰器

装饰器除了能够用在class类上,还可以用在方法上。

例如:

  // 方法装饰器

  const helloDecorator: MethodDecorator = (
    target: Object,
    propertyKey: string | symbol,
    descriptor: PropertyDescriptor
  ) => {
    // console.log(target) // target是原型
    // console.log(propertyKey) // propertyKey 是方法名
    // console.log(descriptor) // desscriptor 是该方法的当前描述符,.value 之后可以得到方法全部代码体

    descriptor.value = (username: string): string  => {
      return username + ',hello啊'
    }
  };

  class Person {
    @helloDecorator // 使用方法装饰器
    sayHello(us: string): void {}
    move() {}
  }

  const p = new Person()
  console.log(p.sayHello('zs'))

方法装饰器示例

// 利用装饰器实现延迟执行

  // 定义一个方法装饰器
  const ResponseMessageDecoratorFactory = (
    time: number
  ): MethodDecorator => {
    return (...rests: any[]) => {
      const [, , descriptor] = rests;
      const method = descriptor.value;
      descriptor.value = () => {
        setTimeout(() => {
          method()
        }, time);
      };
    };
  };

  class Person {
    @ResponseMessageDecoratorFactory(2000)
    run() {
      console.log("running .....");
    }
  }

  const p = new Person
  p.run()

属性修饰器

// 属性修饰器
  let _value: string = ''
  const toUpperCaseDecorator: PropertyDecorator = (
    target: Object,
    propertyKey: string | symbol
  ) => {
    // console.log(target) // target 原型
    // console.log(propertyKey) // 属性名

    // 在其中进行数据劫持
    Object.defineProperty(target, propertyKey, {
      get() {
        return _value.toUpperCase()
      },

      set(val) {
        _value = val
      }
    })

  };

  class Person {
    // 定义一个属性以及使用修饰器
    @toUpperCaseDecorator
    public username: string;
    constructor(uname: string) {
      // 对username进行初始化
      this.username = uname;
    }
  }

  const p = new Person('zs')
  console.log(p.username)

参数修饰器

const ParamsDecorator: ParameterDecorator = (
		target: Object,
		propertyKey: string | symbol,
		parameterIndex: number
	) => {
		console.log(parameterIndex);
	};
	// 属性装饰器和参数装饰器
	class Player {
		// 创建一个属性
		public username: string = 'admin';

		//参数修饰器
		public run(idx: number, @ParamsDecorator keywords: string): void {
			console.log('run方法正在被执行');
			// console.log(this.username)
		}
	}
	new Player().run(100, '小明');

TS练习

元素拖拽:

// 获取元素
const app: HTMLDivElement = document.querySelector("#app") as HTMLDivElement;

class Move {
  public el: HTMLDivElement;

  // 鼠标按下时,元素距离页面的位置距离
  private mousedownX: number = 0;
  private mousedownY: number = 0;

  // 鼠标按下时,鼠标与页面的距离
  private mouseX: number = 0;
  private mouseY: number = 0;

  constructor(dom: HTMLDivElement) {
    this.el = dom;
  }

  init(): void {
    this.el.onmousedown = (e: MouseEvent) => {
      // 监听window身上的事件
      this.bindEvent(e);
    };

    this.el.onmouseup = () => {
      this.stop();
    };
  }

  // 给元素监听事件
  bindEvent(e: MouseEvent): void {
    this.mousedownX = this.el.offsetLeft;
    this.mousedownY = this.el.offsetTop;

    this.mouseX = e.pageX;
    this.mouseY = e.pageY;

    // 调用move方法
    this.move();
  }

  // 让元素运动起来
  move(): void {
    window.onmousemove = (e: MouseEvent) => {
      let distanceX: number = e.pageX - this.mouseX;
      let distanceY: number = e.pageY - this.mouseY;

      this.el.style.left = this.mousedownX + distanceX + "px";
      this.el.style.top = this.mousedownY + distanceY + "px";
    };
  }

  stop(): void {
    window.onmousemove = null;
  }
}

const m = new Move(app);
m.init();
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Document</title>
  <style>
    #app {
      width: 100px;
      height: 100px;
      background-color:red;
      position: absolute;
    }
  </style>
</head>
<body>
  <div id="app" style="top: 0;left: 0;"></div>
</body>
<script src="../dist/07.js"></script>
</html>