再学TypeScript

175 阅读10分钟

数据类型

函数

"strictPropertyInitialization": true 启用类属性初始化的严格检查

/**
 * 当我们写一个类的时候,会得到2个类型
 * 1. 构造函数类型的函数类型
 * 2. 类的实例类型
 */
class Component {
  static myName: string = '静态名称属性'
  myName: string = '实例名称属性'
}
let com = Component
//Component类名本身表示的是实例的类型
//ts 一个类型 一个叫值
//冒号后面的是类型
//放在=后面的是值
let c: Component = new Component()
let f: typeof Component = com

readonly

  • readonly修饰的变量只能在构造函数中初始化
  • 允许将 interface、type、 class 上的属性标识为 readonly
  • readonly 实际上只是在编译阶段进行代码检查。而 const 则会在运行时检查(在支持 const 语法的 JavaScript 运行时环境中)
class Animal {
  public readonly name: string
  constructor(name: string) {
    this.name = name
  }
  changeName(name: string) {
    this.name = name // error
  }
}

let a = new Animal('lc')
a.changeName('cl')

修饰符

class Father {
  public name: string //类里面 子类 其它任何地方外边都可以访问
  protected age: number //类里面 子类 都可以访问,其它任何地方不能访问
  private money: number //类里面可以访问, 子类和其它任何地方都不可以访问
  constructor(name: string, age: number, money: number) {
    //构造函数
    this.name = name
    this.age = age
    this.money = money
  }
  getName(): string {
    return this.name
  }
  setName(name: string): void {
    this.name = name
  }
}
class Child extends Father {
  constructor(name: string, age: number, money: number) {
    super(name, age, money)
  }
  desc() {
    console.log(`${this.name} ${this.age} ${this.money}`)
  }
}

let child = new Child('lc', 10, 1000)
console.log(child.name)
console.log(child.age) // error
console.log(child.money) // error

抽象类

  • 抽象描述一种抽象的概念,无法被实例化,只能被继承
  • 无法创建抽象类的实例
  • 抽象方法不能在抽象类中实现,只能在抽象类的具体子类中实现,而且必须实现
abstract class Animal {
  name!: string
  abstract speak(): void
}
class Cat extends Animal {
  speak() {
    console.log('喵喵喵')
  }
}
let animal = new Animal() // 无法创建抽象类的实例
animal.speak()
let cat = new Cat()
cat.speak()

抽象方法

  • 抽象类和方法不包含具体实现,必须在子类中实现
  • 抽象方法只能出现在抽象类中
  • 子类可以对抽象类进行不同的实现
abstract class Animal {
  abstract speak(): void
}
class Dog extends Animal {
  speak() {
    console.log('汪汪汪')
  }
}
class Cat extends Animal {
  speak() {
    console.log('喵喵喵')
  }
}
let dog = new Dog()
let cat = new Cat()
dog.speak()
cat.speak()

重写(override) vs 重载(overload)

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

继承 vs 多态

  • 继承(Inheritance)子类继承父类,子类除了拥有父类的所有特性外,还有一些更具体的特性
  • 多态(Polymorphism)由继承而产生了相关的不同的类,对同一个方法可以有不同的行为

装饰器

  • 装饰器是一种特殊类型的声明,它能够被附加到类声明、方法、属性或参数上,可以修改类的行为
  • 常见的装饰器有类装饰器、属性装饰器、方法装饰器和参数装饰器
  • 装饰器的写法分为普通装饰器和装饰器工厂

装饰器使用 @expression 的形式,其中 expression 必须能够演算为在运行时调用的函数,其中包括装饰声明信息。在不改变对象自身的基础上,动态增加额外的职责。把对象核心职责和要装饰的功能分开了。非侵入式的行为修改。 image.png Typescript 中的装饰器 expression 求值后为一个函数,它在运行时被调用,被装饰的声明信息会被做为参数传入。

class Person {
  @time
  say() {
    console.log('hello')
  }
}

Javascript规范里的装饰器目前处在 建议征集的第二阶段,github.com/tc39/propos… 也就意味着不能在原生代码中直接使用,浏览器暂不支持。 TypeScript 工具在编译阶段,把装饰器语法转换成浏览器可执行的代码。

// tsconfig 中开启
"experimentalDecorators": true

装饰器分类

类装饰器

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

属性装饰器

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

参数装饰器

  • 会在运行时当作函数被调用,可以使用参数装饰器为类的原型增加一些元数据
    • 第1个参数对于静态成员是类的构造函数,对于实例成员是类的原型对象
    • 第2个参数的名称
    • 第3个参数在函数列表中的索引

装饰器执行顺序

  • 有多个参数装饰器时(复合装饰):从最后一个参数依次向前执行
  • 方法和方法参数中参数装饰器先执行。
  • 类装饰器总是最后执行
  • 方法和属性装饰器,谁在前面谁先执行。因为参数属于方法一部分,所以参数会一直紧紧挨着方法执行
  • 类比React组件的componentDidMount 先上后下、先内后外

装饰器执行演示

function d1(target: Function) {
  console.log('---------------d1类装饰器---------------------')
  console.log(target)
  console.log(typeof target)
}

function d2(target: any, name: string) {
  console.log('---------------d2属性装饰器---------------------')
  console.log(typeof target, name)
}

function d3(target: any, name: string, descriptor: PropertyDescriptor) {
  console.log('---------------d3访问器装饰器---------------------')
  console.log(typeof target, name)
  console.log(descriptor)
}
function d4(target: any, name: string, descriptor: PropertyDescriptor) {
  console.log('---------------d4方法装饰器---------------------')
  console.log(typeof target, name)
  console.log(descriptor)
}
function d5(target: any, name: string, index: number) {
  // name 是当前参数所在的方法
  console.log('---------------d5参数装饰器---------------------')
  console.log(typeof target, name)
  console.log(index)
}

@d1
class MyClass {
  @d2
  static property1: number

  @d2
  a: number

  @d3
  get b() {
    return 1
  }
  @d3
  static get c() {
    return 2
  }

  @d4
  public method1(@d5 x: number, @d5 y: number) {}
  @d4
  public static method2() {}
}
---------------d2属性装饰器---------------------
object a
---------------d3访问器装饰器---------------------
object b
{
get: [Function: get],
set: undefined,
enumerable: false,
configurable: true
}
---------------d5参数装饰器---------------------
object method1
1
---------------d5参数装饰器---------------------
object method1
0
---------------d4方法装饰器---------------------
object method1
{
value: [Function],
writable: true,
enumerable: true,
configurable: true
}
---------------d2属性装饰器---------------------
function property1
---------------d3访问器装饰器---------------------
function c
{
get: [Function: get],
set: undefined,
enumerable: false,
configurable: true
}
---------------d4方法装饰器---------------------
function method2
{
value: [Function],
writable: true,
enumerable: true,
configurable: true
}
---------------d1类装饰器---------------------
[Function: MyClass] { method2: [Function] }
function

TS装饰器原理

  • 装饰器本质就是一个函数
  • 利用被装饰目标的prototype来对其进行扩展
function Path(baseUrl: string) {
    return function (target) {
        target.prototype.$Meta = {
            baseUrl: baseUrl
        }
    }
}

接口interface

  • interface中可以用分号或者逗号分割每一项,也可以什么都不加

对象接口

//接口可以用来描述`对象的形状`,少属性或者多属性都会报错
interface Speakable {
  speak(): void
  name?: string //?表示可选属性
}

let speakman: Speakable = {
  speak() {}, //少属性会报错
  name,
  age, //多属性也会报错
}

行为抽象

//接口可以在面向对象编程中表示为行为的抽象
interface Speakable {
  speak(): void
}
interface Eatable {
  eat(): void
}
//一个类可以实现多个接口
class Person implements Speakable, Eatable {
  speak() {
    console.log('Person说话')
  }
  eat() {}
}
class TangDuck implements Speakable {
  speak() {
    console.log('TangDuck说话')
  }
  eat() {}
}

任意属性

//无法预先知道有哪些新的属性的时候,可以使用 `[propName:string]:any`,propName名字是任意的
interface Person {
  readonly id: number
  name: string
  [propName: string]: any
}

let p1 = {
  id: 1,
  name: 'lc',
  age: 10,
}

接口继承

interface Speakable {
  speak(): void
}
interface SpeakChinese extends Speakable {
  speakChinese(): void
}
class Person implements SpeakChinese {
  speak() {
    console.log('Person')
  }
  speakChinese() {
    console.log('speakChinese')
  }
}

可索引接口


interface UserInterface {
  [index: number]: string
}
let arr: UserInterface = ['value1', 'value2']
console.log(arr)

interface UserInterface2 {
  [index: string]: string
}
let obj: UserInterface2 = { name: 'value' }

类接口

interface Speakable {
  name: string
  speak(words: string): void
}
class Dog implements Speakable {
  name!: string
  speak(words: string) {
    console.log(words)
  }
}
let dog = new Dog()
dog.speak('汪汪汪')

命名空间

  • 在代码量较大的情况下,为了避免命名空间冲突,可以将相似的函数、类、接口放置到命名空间内
  • 命名空间可以将代码包裹起来,只对外暴露需要在外部访问的对象,命名空间内通过export向外导出
  • 命名空间是内部模块,主要用于组织代码,避免命名冲突
export namespace zoo {
  export class Dog {
    eat() {
      console.log('zoo dog')
    }
  }
}
export namespace home {
  export class Dog {
    eat() {
      console.log('home dog')
    }
  }
}
let dog_of_zoo = new zoo.Dog()
dog_of_zoo.eat()
let dog_of_home = new home.Dog()
dog_of_home.eat()

原理

  • 其实一个命名空间本质上一个对象,它的作用是将一系列相关的全局变量组织到一个对象的属性
namespace Numbers {
  export let a = 1
  export let b = 2
  export let c = 3
}

var Numbers
;(function (Numbers) {
  Numbers.a = 1
  Numbers.b = 2
  Numbers.c = 3
})(Numbers || (Numbers = {}))

可合并

namespace k1 {
  let a = 10
  export var obj = {
    a,
  }
}
namespace k1 {
  let b = 20
  export var obj2 = {
    b,
  }
}
namespace k2 {
  console.log(k1)
}

类型声明

  • 声明文件可以让我们不需要将JS重构为TS,只需要加上声明文件就可以使用系统
  • 类型声明在编译的时候都会被删除,不会影响真正的代码
  • 关键字 declare 表示声明的意思,我们可以用它来做出各种声明:
declare let b: {
    v: number
}

export default g

TypeScript 模块解析策略

TypeScript 现在使用了与 Node.js 类似的模块解析策略,但是 TypeScript 增加了其它几个源文件扩展名的查找(.ts、.tsx、.d.ts),同时 TypeScript 在 package.json 里使用字段 types 来表示查找路径

泛型

为什么要用泛型

  • 许多时候,标注的具体类型并不能确定,比如一个函数的参数类型
  • 这个时候这种定义过程不确定类型的需求就可以通过泛型来解决
  • 泛型(Generics)是指在定义函数、接口或类的时候,不预先指定具体的类型,而在使用的时候再指定类型的一种特性
  • 泛型T作用域只限于函数内部使用

泛型函数

  • 首先,我们来实现一个函数 createArray,它可以创建一个指定长度的数组,同时将每一项都填充一个默认值
function createArray(length: number, value: any): Array<any> {
  let result: any = []
  for (let i = 0; i < length; i++) {
    result[i] = value
  }
  return result
}
let result = createArray(3, 'x')

如果像这样将传入参数设置为any类型,不能得到准确的返回类型 如果使用泛型,我们就能约束我们想要的结果

function createArray2<T>(length: number, value: T): Array<T> {
  let result: T[] = []
  for (let i = 0; i < length; i++) {
    result[i] = value
  }
  return result
}
let result2 = createArray2<string>(3, 'x')

泛型类

class MyArray<T> {
  private list: T[] = []
  add(value: T) {
    this.list.push(value)
  }
  getMax(): T {
    let result = this.list[0]
    for (let i = 0; i < this.list.length; i++) {
      if (this.list[i] > result) {
        result = this.list[i]
      }
    }
    return result
  }
}
let arr = new MyArray<number>()
arr.add(1)
arr.add(2)
arr.add(3)
let ret = arr.getMax()
console.log(ret)

泛型与new

function factory<T>(type: { new (): T }): T {
  return new type()
}

const arr = factory<string[]>(Array)

泛型接口

  • 泛型接口可以用来约束函数

后端提供了一些接口,用以返回一些数据,依据返回的数据格式定义如下接口:

interface IResponseData {
    code: number;
    message?: string;
    data: any;	// 现在无法确定接口数据类型
}
  • 但是,我们会发现该接口的 data 项的具体类型不确定,不同的接口会返回的数据是不一样的
  • 当我们想根据具体当前请求的接口返回具体 data 格式的时候,就比较麻烦了,因为 getData 并不清楚你调用的具体接口是什么,对应的数据又会是什么样的
  • 这个时候我们可以对 接口数据IResponseData 使用泛型
interface IResponseData<T> {
  code: number
  message?: string
  data: T
}

// 用户接口
interface IResponseUserData {
  id: number
  username: string
  email: string
}
// 文章接口
interface IResponseArticleData {
  id: number
  title: string
  author: IResponseUserData
}

async function getData<U>(url: string) {
  let response = await fetch(url)
  let data: Promise<IResponseData<U>> = await response.json()
  return data
}

;(async function () {
  let userData = await getData<IResponseUserData>('/user')
  userData.data.id

  let articleData = await getData<IResponseArticleData>('/article')
  articleData.data.author
})()

多个泛型

function swap<A, B>(tuple: [A, B]): [B, A] {
  return [tuple[1], tuple[0]]
}
let swapped = swap<string, number>(['a', 1])
console.log(swapped)
console.log(swapped[0].toFixed(2))
console.log(swapped[1].length)

泛型约束

  • 在函数中使用泛型的时候,由于预先并不知道泛型的类型,所以不能随意访问相应类型的属性或方法。
function logger<T>(val: T) {
  console.log(val.length) //直接访问会报错
}
//可以让泛型继承一个接口
interface LengthWise {
  length: number
}
//可以让泛型继承一个接口
function logger2<T extends LengthWise>(val: T) {
  console.log(val.length)
}
logger2('acg')
logger2(1)

类型保护

typeof

function double(input: string | number | boolean) {
    if (typeof input === 'string') {
        return input + input;
    } else {
        if (typeof input === 'number') {
            return input * 2;
        } else {
            return !input;
        }
    }
}

instanceof

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

链判断运算符

  • 链判断运算符是一种先检查属性是否存在,再尝试访问该属性的运算符,其符号为 ?.
  • 如果运算符左侧的操作数 ?. 计算为 undefined 或 null,则表达式求值为 undefined 。否则,正常触发目标属性访问,方法或函数调用。
a?.b //如果a是null/undefined,那么返回undefined,否则返回a.b的值.
a == null ? undefined : a.b

a?.[x] //如果a是null/undefined,那么返回undefined,否则返回a[x]的值
a == null ? undefined : a[x]

a?.b() // 如果a是null/undefined,那么返回undefined
a == null ? undefined : a.b() //如果a.b不函数的话抛类型错误异常,否则计算a.b()的结果

a?.() //如果a是null/undefined,那么返回undefined
a == null ? undefined : a() //如果A不是函数会抛出类型错误
//否则 调用a这个函数

in操作符

  • in 运算符可以被用于参数类型的判断
interface Bird {
  swing: number
}

interface Dog {
  leg: number
}

function getNumber(x: Bird | Dog) {
  if ('swing' in x) {
    return x.swing
  }
  return x.leg
}

unknown类型

unknown vs any

  • unknown 和 any 的主要区别是 unknown 类型会更加严格:在对 unknown 类型的值执行大多数操作之前,我们必须进行某种形式的检查。而在对 any 类型的值执行操作之前,我们不必进行任何检查
  • 任何类型都可以赋值给unknown和any类型
  • unknown类型只能被赋值给any类型和unknown类型本身

联合类型中的 unknown 类型

  • 在联合类型中,unknown 类型会吸收任何类型。这就意味着如果任一组成类型是
type UnionType1 = unknown | null;       // unknown
type UnionType2 = unknown | undefined;  // unknown
type UnionType3 = unknown | string;     // unknown
type UnionType4 = unknown | number[];   // unknown

交叉类型中的 unknown 类型

  • 在交叉类型中,任何类型都可以吸收 unknown 类型。这意味着将任何类型与 unknown 相交不会改变结果类型
type IntersectionType1 = unknown & null;       // null
type IntersectionType2 = unknown & undefined;  // undefined
type IntersectionType3 = unknown & string;     // string
type IntersectionType4 = unknown & number[];   // number[]
type IntersectionType5 = unknown & any;        // any

never是unknown的子类型

type isNever = never extends unknown ? true : false;

keyof unknown 等于never

type key = keyof unknown;

只能对unknown进行等或不等操作,不能进行其它操作

un1===un2;
un1!==un2;
un1 += un2;

不能做任何操作

  • 不能访问属性
  • 不能作为函数调用
  • 不能当作类的构造函数不能创建实例
un.name
un();
new un();

映射属性

  • 如果映射类型遍历的时候是unknown,不会映射属性
type getType<T> = {
  [P in keyof T]:number
}
type t = getType<unknown>;

类型变换

交叉类型

  • 交叉类型(Intersection Types)是将多个类型合并为一个类型
  • 这让我们可以把现有的多种类型叠加到一起成为一种类型,它包含了所需的所有类型的特性
interface Bird {
  name: string
  fly(): void
}
interface Person {
  name: string
  talk(): void
}
type BirdPerson = Bird & Person
let p: BirdPerson = { name: 'lc', fly() {}, talk() {} }
p.fly
p.name
p.talk

联合类型的交叉类型

type Ta = string | number
type Tb = number | boolean
type Tc = Ta & Tb // number

mixin

mixin混入模式可以让你从两个对象中创建一个新对象,新对象会拥有着两个对象所有的功能

interface AnyObject {
  [prop: string]: any
}

function mixin<T extends AnyObject, U extends AnyObject>(
  one: T,
  two: U,
): T & U {
  const result = <T & U>{}
  for (let key in one) {
    ;(<T>result)[key] = one[key]
  }
  for (let key in two) {
    ;(<U>result)[key] = two[key]
  }
  return result
}

const x = mixin({ name: 'lc' }, { age: 18 })
console.log(x.name, x.age)

keyof

  • 索引类型查询操作符
interface Person {
  name: string
  age: number
  gender: 'male' | 'female'
}
//type PersonKey = 'name'|'age'|'gender';
type PersonKey = keyof Person // 得到字面量类型

function getValueByKey(p: Person, key: PersonKey) {
  return p[key]
}
let val = getValueByKey({ name: 'lc', age: 18, gender: 'male' }, 'name')
console.log(val)

条件类型

定义条件类型

interface Fish {
  name: string
}
interface Water {
  name: string
}
interface Bird {
  name: string
}
interface Sky {
  name: string
}
//若 T 能够赋值给 Fish,那么类型是 Water,否则为 Sky
type Condition<T> = T extends Fish ? Water : Sky
let condition: Condition<Fish> = { name: '水' }

  • 找出T类型中U不包含的部分
//never会被自动过滤
type Diff<T, U> = T extends U ? never : T

type R = Diff<'a' | 'b' | 'c' | 'd', 'a' | 'c' | 'f'> // "b" | "d"

type Filter<T, U> = T extends U ? T : never
type R1 = Filter<string | number | boolean, number>

内置条件类型

Exclude
  • 从 T 可分配给的类型中排除 U
// type Exclude<T, U> = T extends U ? never : T
type E = Exclude<string | number, string> // 排除string类型
let e: E = 10
Extract
  • 从 T 可分配的类型中提取 U
// type Extract<T, U> = T extends U ? T : never

type E = Extract<string | number, string> // 可以理解为交集
let e: E = '1'

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

type E = NonNullable<string | number | null | undefined>
let e: E = null
ReturnType
  • infer最早出现在此 PR 中,表示在 extends 条件语句中待推断的类型变量
  • 获取函数类型的返回类型
type ReturnType<T extends (...args: any[]) => any> = T extends (
  ...args: any[]
) => infer R
  ? R
  : any

function getUserInfo() {
  return { name: 'lc', age: 18 }
}

// 通过 ReturnType 将 getUserInfo 的返回值类型赋给了 UserInfo
type UserInfo = ReturnType<typeof getUserInfo>
/* 
type UserInfo = {
    name: string;
    age: number;
}
*/
const userA: UserInfo = {
  name: 'lc',
  age: 18,
}

内置工具类型

Partial 可选转换

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

interface A {
  a1: string
  a2: number
  a3: boolean
}

type aPartial = Partial<A>

const a: aPartial = {} // 不会报错

原理

type Partial<T> = { [P in keyof T]?: T[P] }

// in 可以理解为 for ... in,表示从 keyof T 中去遍历每一个类型

用js伪代码模拟

const newType = {}
Object.keys(T).forEach(key => {
  newType[key] = !T[key]
})

其他类似的还有

  • Required 必选转换
  • Readonly 只读转换

DeepPartial 类型递归

将递归类型展开

interface Company {
  id: number
  name: string
}

interface Person {
  id: number
  name: string
  company: Company
}
type R2 = DeepPartial<Person>

原理

type DeepPartial<T> = {
  [U in keyof T]?: T[U] extends object ? DeepPartial<T[U]> : T[U]
}

Pick

  • Pick 能够帮助我们从传入的属性中摘取某一项或多项返回
/**
 * From T pick a set of properties K
 * type Pick<T, K extends keyof T> = { [P in K]: T[P] };
 */
// 摘取 Person 中的 name 和 age 属性
interface Person {
  name: string
  age: number
  married: boolean
}

let person: Person = { name: 'lc', age: 18, married: false }
let result: Pick<Person, 'name' | 'age'> = {
  name: 'lc',
  age: 18,
}
console.log(result)

Record

  • Record 是 TypeScript 的一个高级类型
  • 他会将一个类型的所有属性值都映射到另一个类型上并创造一个新的类型
/**
 * Construct a type with a set of properties K of type T
 */
type Record<K extends keyof any, T> = {
    [P in K]: T;
};
type Point = 'x' | 'y'
type PointList = Record<Point, { value: number }>

type PointList = {
    x: {
        value: number;
    };
    y: {
        value: number;
    };
}