ts基础 类型 、泛型、装饰器(含demo)

114 阅读16分钟

基础概念

TS 是在 JavaScript 语法的基础上进行扩展和增强的超集,尤其在类型系统方面,为开发者提供了更严格和明确的类型定义和约束。

面向项目

  • TS 主要面向解决大型复杂项目,其架构和代码维护较为复杂。例如大型企业级应用、多人协作开发的项目等。
  • JS 是脚本化语言,常用于面向简单页面场景。

自主检测

  • TS 在编译时能主动发现并纠正错误。
  • JS 往往在运行时才报错。

类型检测

  • TS 是强类型语言,支持类型检测,并且可以在编译时进行类型检查和提示。
  • JS 是弱类型语言,无静态类型选项。

运行流程

  • TS 依赖编译,编译后的产物(通常是 JavaScript 代码)最终在浏览器中运行。
  • JS 可直接在浏览器中运行。

检测阶段

  • 在 TypeScript 中,大部分的类型检测纠错是在编译阶段进行的。
  • 但是在一些特定的运行时环境(如某些 Node.js 应用)中,也可能存在一些运行时的类型检查机制或工具。

安装运行

安装

显示版本就安装成功了,也可以选择项目局部安装

    npm install -g typescript
    tsc -v

运行编译

tsc demo.ts

TS基础类型和写法

常用数据

boolean | string | number | array | null | undefined | Array | ReadonlyArray |object

//js
let isEnabled = true
let course = 'haha'
let classNum = 2
let u = undefined
let n = null
let classArr = ['basic', 'execute']

//ts
let isEnabled: boolean = true
let course: string = 'haha'
let classNum: number = 2
let u: undefined = undefined
let n: null = null
//数组
//let classArr: string[] = ['basic', 'execute']
let classArr: Array<string> = ['basic', 'execute']


let arr: number[] = [1, 3, 4, 5, 6]
let ro: ReadonlyArray<number> = arr

arr.push(7) // OK
ro[0] = 12   // 赋值 - Error
ro.push(7)   // 增加 - Error
ro.length = 100 // 长度改写 - Error

object 类型用于表示非原始类型的值,即不是 number 、 string 、 boolean 、 null 或 undefined 的类型。

let obj: object = { name: 'John' }; 
let arr: object = [1, 2, 3]; 
let func: object = function() {}; 

object 类型相对比较宽泛,不能准确指定对象的具体属性和结构。 如果您想要更精确地描述对象的结构和属性,可以使用接口(interface )或类型别名(type )来定义特定的对象类型。

它包括对象、数组、函数以及其他更复杂的数据结构。

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

let person: Person = { name: 'John', age: 30 };

tuple - 元组

let tupleType: [string, boolean];

tupleType = ['haha', true];

enum - 枚举

以下例子的ScoreRes变量,均为demo.ts执行tsc编译后,Score的值

默认从0开始枚举
enum Score {
  A,
  B,
  C,
  D
}
let score: Score = Score.A
console.log(score);   //0
console.log(Score[0]);//A
console.log(Score.A);//0

const ScoreRes = 
    {
      '0': 'A',
      '1': 'B',
      '2': 'C',
      '3': 'D',
      A: 0,
      B: 1,
      C: 2,
      D: 3
    }
定义数值后,会往后依次递增
//enum Score {
//   A=-2,
//   B,
//   C,
//   D='eee',//这里会导致后面报错,要么连着E,F也一起定义值
//   E,
//   F
// }

enum Score {
  A=-2,
  B,
  C,
  D=7,
  E,
  F
}
let score: Score = Score.A
console.log(score);   //-2
console.log(Score[0]);//C
console.log(Score[8]);//E
console.log(Score.C);//0
const ScoreRes = 
{
    '-2': 'A',
    '-1': 'B',
    '0': 'C',
    '7': 'D',
    '8': 'E',
    '9': 'F',
    A: -2,
    B: -1,
    C: 0,
    D: 7,
    E: 8,
    F: 9
}

没有按照从小到大定义数值的情况
  • 会导致枚举异常,丢失value对key的索引,建议还是从小到大定义枚举值
enum Score {
  A=2,
  B,
  C,
  D=1,
  E,
  F
}

const ScoreRes = 
    {
      '1': 'D',
      '2': 'E',
      '3': 'F',
      '4': 'C',
      A: 2,
      B: 3,
      C: 4,
      D: 1,
      E: 2,
      F: 3
}

几个特殊类型

anyunknownvoid | never

any

any 类型是一种非常灵活但也需要谨慎使用的类型。

any 类型允许您对变量进行任何操作,忽略类型检查。这意味着您可以将任何值赋给 any 类型的变量,并且可以对其执行任何操作,而 TypeScript 编译器不会给出类型错误提示

//绕过所有的类型检查
let anyValue: any = 123
anyValue = 'anyValue'
anyValue = false
anyValue = new Error()
unknown

unknown 类型是一种安全的、类型未知的类型。

与 any 类型不同,unknown 类型更加安全和严谨。当一个变量被声明为 unknown 类型时,您不能对其进行直接的操作,除非先进行类型断言或类型缩小。

  • unknown 类型表示的是未知的类型,不能直接将其赋值给(value1)特定的类型,如 boolean 类型。
  • 将一个值声明为 unknown 类型时,TypeScript 编译器不知道这个值的确切类型和结构。所以,直接将其赋值给明确的类型(如 boolean )是不被允许的,因为无法确定这个 unknown 值是否真的能兼容 boolean 类型。
let unknownValue: unknown

unknownValue = 123
unknownValue = 'unknownValue'
unknownValue = true

let value: unknown;
if (typeof value ==='string') {
  value.toUpperCase(); // 此时可以操作,因为经过类型判断确定为字符串类型
}
let value1: boolean = unknownValue  // NOK
let value2: any = unknownValue  // OK
let value3: unknown = unknownValue  // OK
void

在 TypeScript 中,void 类型用于表示函数没有返回值的情况。

  • 例如,如果一个函数只是执行一些操作而不返回任何有意义的值,它的返回类型可以被指定为 void :
function printMessage(message: string): void {
     console.log(message);
}
never

never 类型表示那些永远不会有返回值的函数的返回类型。

  • 函数中存在无限循环或者抛出异常,导致函数永远无法正常结束
function infiniteLoop(): never { 
    while (true) {}
}
  • 函数的分支涵盖了所有可能的情况,并且这些分支最终都会抛出错误或者进入无限循环。
function error(message: string): never {
    throw new Error(message);
}

定义类型

interface(接口)

  • 主要用于定义对象的形状和行为。
  • 支持声明合并,即可以多次声明同一个接口,新的声明会与之前的声明合并。
  • 更适合描述具有公共结构的对象类型。
interface Person {
  readonly name: string; //只读
  age?: number; //可选
}

interface Person {
  occupation: string;
  [propName: number]: number;//
}

let person: Person = {
  name: 'John',
  //age: 30,//可有可无
  occupation: 'Developer'1:1//合法
};

propName 在上面只是一个占位符,表示具有数字类型索引(键)的属性。

这意味着可以为这个 Class 类型的对象添加任意数量的以数字为键的属性,并且这些属性的值可以是任何类型(由 any 指定)。

extends-继承

extends 关键字用于类的继承。通过继承,可以在子类中复用父类的属性和方法,并可以添加新的属性和方法或者重写父类的方法。


    interface Class {
        name: string
    }
    interface Course1 {
    
    }
    
    interface Course2 extends Class {
        score: number
    }
  • 也有说可以用于类型缩小的说法; 例如,假设有一个父类 BaseClass 和一个子类 DerivedClass 继承自 BaseClass :
class BaseClass {
  baseProperty: string;
}

class DerivedClass extends BaseClass {
  derivedProperty: number;
}

function process(obj: BaseClass) {
  if (obj instanceof DerivedClass) {
    // 在这里,由于通过 instanceof 进行了类型判断,实现了类型缩小
    // 可以安全地访问 DerivedClass 特有的属性 derivedProperty
    console.log((obj as DerivedClass).derivedProperty); 
  }
}

在上述示例中,通过 instanceof 操作符进行类型判断,如果对象是 DerivedClass 的实例,就实现了类型的缩小,从而可以在后续代码中访问子类特有的属性或方法。

type(类型别名)

  • 可以用于定义各种类型,不仅仅是对象类型,还包括联合类型、交叉类型、元组等更复杂的类型。
  • 不支持声明合并,重复定义会报错。
type Person = {
  name: string;
  age: number;
};

type MyUnion = string | number;
重复定义
     type Class = {
        name: string
    }
    
    type Class = {//会报错,标识符“Class”重复。
        score: number
    }

交叉类型"&" + 联合类型"|"

    interface A { x: D }
    interface B { x: E }
    interface C { x: F }

    interface D { d: boolean }
    interface E { e: string }
    interface F { f: number }

    type ABC = A & B | C
    let abc: ABC = {
        x: {
            d: false,
            e: 'ts',
            //f: 5 //可选
        }
    }
    
    type Class = {
        name: string
    }
    type Course = {
        nickName: string
    }

    type NameObj = Class | Course
    let nameObj:NameObj={name:'小明'}//{ nickName: '阿明' }

    type DetailObj = Class & Course
    let detailObj:DetailObj={name:'小明', nickName: '阿明'}

    type TupleClass = [Class, Course]
    let  arr:TupleClass=[{name:'小明'}, { nickName: '阿明' }]
灵活性
    type Keys = 'name' | 'nickName'

    type Class = {
        [key in Keys]: string
    }

    const haha: Class = {
        name: 'typescript',
        nickName: 'ts'
    }

type和interface的区别

相同点
  1. 都用于定义类型:它们的主要目的都是为了给数据结构或类型提供明确的定义,以增强代码的类型安全性和可读性。
  2. 可以描述对象结构:两者都能够描述对象应该具有的属性及其类型。
  3. 可用于函数类型定义:都可以用来定义函数的参数和返回值的类型。
  4. 作为类型约束:在函数参数、变量声明等地方,可以作为类型约束来确保数据的类型符合预期。
  5. 提升代码的可维护性和可理解性:通过清晰地定义类型,使代码更易于理解和维护,减少类型相关的错误。
不同点
  1. 定义对象类型的方式:interface 主要用于定义对象的形状和结构,而 type 不仅可以定义对象类型,还能定义联合类型、交叉类型、元组等更复杂的类型。
  2. 声明合并:interface 支持声明合并,即可以多次声明同一个接口,新的声明会与之前的声明合并。而 type 不支持声明合并。
  3. 灵活性:type 更加灵活,能处理更广泛和复杂的类型定义情况。
  4. 应用场景:interface 更适合描述具有公共结构的对象类型,type 则在需要定义复杂或特殊类型结构时更具优势。

总的来说,interface 侧重于对象类型的定义和扩展,type 则在类型定义的多样性和灵活性方面表现出色,选择使用哪种方式取决于具体的需求和项目的代码风格。

类型不兼容/类型冲突

    interface A {
        c: string;
        d: string;
    }

    interface B {
        c: number;
        e: string;
    }
    //合并c的关系 => c: never
    type AB = A & B //错误写法c的值不可能是string的同时也是number


    type AA = {
        c: string;
        d: string;
    }

    type BB =  {
        c: number;
        e: string;
    }

    type AABB =AA & BB
    const aabb:AABB = {d:'d',e:'e',c:"1"} // 不能将类型“string”分配给类型“never”。

断言

类型断言的两种形式

  1. “尖括号”语法:<类型>值
  2. as 语法:值 as 类型
    // 尖括号声明 阶段性的类型
    let anyValue: any = 'haha'
    let anyLength: number = (<string>anyValue).length

    // as声明
    let anyValue: any = 'haha'
    let anyLength: number = (anyValue as string).length

    // 非空声明
    type ClassTIme = () => number

    const start = (classTime: ClassTime | undefined) => {
        let num = classTime!() // 具体类型可能为多种,但是非空确认
    }

主要用途包括

  1. 当编译器无法自行推断出某个变量的更具体类型时,开发者明确告诉编译器以特定类型来处理该值。
  • 如果从某个复杂的操作中获取了一个值,但其类型不够精确,通过类型断言可以让后续对该值的操作基于更准确的类型。

  1. 在某些情况下,虽然编译器可能知道某个值的类型,但由于类型系统的限制,需要通过断言来访问特定类型的属性或方法。

需要注意的是,类型断言不会执行任何类型转换或检查,只是告诉编译器以指定的类型来处理值。如果断言不正确,在运行时可能会导致错误。

类型守卫

  1. 是用于在运行时确定变量类型的一种机制。

  2. 类型守卫的主要作用是在特定的条件判断中,让 TypeScript 能够更精确地了解变量的类型,从而允许进行更准确的类型检查和智能提示。

常见的类型守卫方式

typeofinstanceof函数自定义

  1. typeof 类型守卫:通过 typeof 操作符来判断基本类型(如 string 、 number 、 boolean 等)。
function processValue(value: string | number) {
  if (typeof value ==='string') {
    // 在这里,TypeScript 知道 value 是字符串类型
    console.log(value.toUpperCase());
  } else {
    // 在这里,TypeScript 知道 value 是数字类型
    console.log(value + 10);
  }
}
  1. instanceof 类型守卫:用于判断一个对象是否是某个类的实例。
class Parent {}
class Child extends Parent {}

function process(obj: Parent) {
  if (obj instanceof Child) {
    // 在这里,TypeScript 知道 obj 是 Child 类型
    console.log('It is a Child instance');
  } else {
    // 在这里,TypeScript 知道 obj 是 Parent 类型
    console.log('It is a Parent instance');
  }
}
  1. 自定义类型守卫:可以通过函数来实现自定义的类型守卫逻辑。
interface Bird {
  fly(): void;
}

interface Dog {
  bark(): void;
}

function isBird(animal: Bird | Dog): animal is Bird {
  return (animal as Bird).fly!== undefined;
}

function processAnimal(animal: Bird | Dog) {
  if (isBird(animal)) {
    animal.fly();
  } else {
    animal.bark();
  }
}

泛型

泛型是一种使函数、类、接口等能够在定义时不指定具体的数据类型,而在使用时再确定类型的机制。

    class GenericClass<T> {
      value: T;

      constructor(value: T) {
        this.value = value;
      }
    }

    interface GenericInterface<T> {
      data: T;
    }

    function startClass<T, U>(name: T, score: U): T {
        return name + score;
    }

    function startClass<T, U>(name: T, score: U): String {
        return name + score;
    }

    function startClass<T, U>(name: T, score: U): T {
        return (name + String(score))  as T
    }
       function startClass<T, U>(name: T, score: U): T {
        return (name + String(score))  as T
    }
    startClass<string, number>('ts', 5)

装饰器

装饰器是一种特殊类型的声明,它能够被附加到类声明,方法, 访问符,属性或参数上。 装饰器使用 @expression 这种形式,expression 求值后必须为一个函数,它会在运行时被调用,被装饰的声明信息做为参数传入。

  • 使用tsc demo.ts编译可能会报错,执行以下热更新脚本可以看到编译后的js
    • tsc fileName.ts --target ES5 -w --experimentalDecorators
  • 装饰器的执行顺序是从下往上,然后从右往左的

是否能携带参数作为区分,可以分为两类装饰器,无参数的装饰器带参数的装饰器

无参数的装饰器

  • 是一个函数,它接收被装饰的目标(比如类、方法、属性等)作为参数,并可以对其进行修改或增强。
    function logClass(target: Function) {
      console.log(`Decorating class: ${target.name}`);
    }

    @logClass
    class MyClass {}

带参数的装饰器

  • 带参数的装饰器是一个返回装饰器函数的函数
function logClassWithParam(message: string) {
  return function(target: Function) {
    console.log(`Decorating class with message: ${message}`);
  };
}

@logClassWithParam('This is a custom message')
class MyClass {}

作用的目标作为区分,可以分为五类装饰器

  • 类装饰器(Class Decorators)
  • 属性装饰器(Property Decorators)
  • 方法装饰器(Method Decorators)
  • 参数装饰器(Parameter Decorators)
  • 访问器装饰器(Accessor Decorators)

类装饰器

应用于类的定义。它可以修改类的构造函数,或者为类添加一些静态属性和方法。

一个参数

类装饰器的参数是类的构造函数。

返回值
  • 类装饰器函数的返回值可以用来修改或替换被装饰的类。
  • 类装饰器函数不返回值(或者返回 undefined ),那么被装饰的类将保持其原始定义不变(返回null会报错)。

如向原型添加方法或属性,都只是对原始类的补充,而不会改变类的整体结构和定义。

以下例子是操作类的原型对象,以及实现单例模型的使用场景

function sealed(constructor: Function) {
  // 在这里可以对类的构造函数进行操作
    Object.seal(constructor);//对MyClass设置为不可扩展
    Object.seal(constructor.prototype);//将MyClass的原型对象设置为不可扩展
}
let instance: any;
//单例模式
function Singleton<T extends { new (...args: any[]): {} }>(constructor: T) {
  if (!instance) {
    instance = new constructor();
  }
  return instance;
}

//@sealed
@Singleton
class MySingletonClass {}

const instance1 = new MySingletonClass();
const instance2 = new MySingletonClass();

console.log(instance1 === instance2); 

属性装饰器

应用于类的属性。可以用于监视、修改或验证属性的行为。

两个参数
  • 目标对象:
    • 如果在实例属性上,目标对象就是类的原型;
    • 如果在静态属性上,目标对象就是类本身。
  • 属性名

以下例子包括实际使用场景验证目标对象

    let ppt,sPpt;
    function readonly(target: Object, propertyKey: string) {
      ppt=target;
      Object.defineProperty(target, propertyKey, {
          writable: false,//不可写入
          value:'jaja'
      });
    }

    function staticProp(target: Object,propertyKey: string) {
      sPpt=target;
    }

    class MyClass {
      @readonly
      public property?: string;

      @staticProp
      static sProperty?:string

    }
    console.log('ppt',MyClass.prototype===ppt);//true,类的原型
    console.log('sPpt',MyClass===sPpt);//true,类本身
    const myClass=new MyClass();
    console.log(myClass.property);//jaja
    myClass.property='haha'
    console.log(myClass.property);//jaja,不可写入

方法装饰器

应用于类的方法。可以修改方法的行为,例如添加日志、性能测量等功能。

方法装饰器有三个参数:

  • 目标对象:如果方法是在实例上,目标对象就是类的原型;如果方法是静态的,目标对象就是类本身。
  • 方法名
  • 属性描述符
属性描述符
  1. value:属性的值(这里是方法本身)。
  2. writable:决定属性值是否可写。如果为 false,则不能重新赋值给该属性。
  3. enumerable:决定属性是否可枚举。如果为 false,在使用 for...in 循环或 Object.keys() 等方法时,该属性将不会被包含。
  4. configurable:决定属性是否可配置。如果为 false,则不能删除该属性,也不能修改其 writable 、 enumerable 和 configurable 特性。 以下例子实现了一个增加函数调用日志的修饰器函数
    function logMethod(target: Object, propertyKey: string, descriptor: PropertyDescriptor) {
      const originalMethod = descriptor.value;

      descriptor.value = function (...args: any[]) {
          console.log(`调用了函数: ${propertyKey}`);
          return originalMethod.apply(this, args);
      };
    }

    class MyClass {
      @logMethod
      public method(msg:string) {
        console.log('msg',msg);// 方法的实现
      }
    }
    new MyClass().method('haha') // 调用了函数: method //msg haha

参数装饰器

应用于方法的参数。可以用于参数的验证、转换等操作。

无法直接访问到参数的值

参数装饰器有三个参数:

  • 目标对象:如果方法是在实例上,目标对象就是类的原型;如果方法是静态的,目标对象就是类本身。
  • 方法名
  • 参数在参数列表中的索引
    function markParameter1(param: string): ParameterDecorator {
      return function(target: any,propertyKey: string | symbol,parameterIndex: number):void {
          console.log('markParameter1-param',param); //  哈哈
          console.log('markParameter1-target',target); // MyClass { method: [Function (anonymous)] }
          console.log('markParameter1-propertyKey',propertyKey); // method
          console.log('markParameter1-parameterIndex',parameterIndex); // 0
      }

    }

    function markParameter2(target: any,propertyKey: string | symbol,parameterIndex: number):void {
          console.log('markParameter2-target',target); // MyClass { method: [Function (anonymous)] }
          console.log('markParameter2-propertyKey',propertyKey); // method
          console.log('markParameter2-parameterIndex',parameterIndex); // 1
    }
    class MyClass {
      //从右往左执行,先调用markParameter2,后调用markParameter1
      method(@markParameter1('哈哈') param1: number, @markParameter2 param2: string) {
        // 方法的实现
      }
    }
    const myClass=new MyClass();

访问器装饰器

类装饰器

应用于类的访问器(getter 和 setter)。

访问器装饰器有三个参数,与方法装饰器相同:

  • 目标对象:如果访问器是在实例上,目标对象就是类的原型;如果访问器是静态的,目标对象就是类本身。
  • 访问器名
  • 属性描述符

使用场景:数据验证和过滤,日志记录,权限控制,数据同步和更新 以下例子为数据限制

    function rangeValidator(min: number, max: number) {
      return function (target: any, propertyKey: string, descriptor: PropertyDescriptor) {
        const originalSetter = descriptor.set;
        descriptor.set = function (value: number) {
          if (value < min) {
            value = min;
          } else if (value > max) {
            value = max;
          }
          originalSetter.call(this, value);
        };

        const originalGetter = descriptor.get;
        descriptor.get = function () {
          const value = originalGetter.call(this);
          return value;
        };
      };
    }

    class Temperature {
      private _temperature: number;

      @rangeValidator(0, 100)
      get temperature() {
        return this._temperature;
      }

      @rangeValidator(0, 100)
      set temperature(newValue: number) {
        this._temperature = newValue;
      }
    }

    const temp = new Temperature();
    temp.temperature = -10;
    console.log(temp.temperature);  // 0
    temp.temperature = 150;
    console.log(temp.temperature);  // 100

日志记录可以在getter,setter内增加log;权限控制可以在getter,setter内对值做鉴权,非法值可以抛出错误等;

参考文档:[ts.nodejs.cn/docs/handbo…]

其他(元数据): 关于元数据直接使用tsc编译,获取以下内置值时会异常的问题;

design:returntype,design:type,design:paramtypes

如果没有启用 emitDecoratorMetadata 选项,编译器不会生成这些元数据,Reflect.getMetadata('design:returntype', People.prototype, 'xxx') 会返回 undefined;

执行以下脚本开启emitDecoratorMetadata

tsc --experimentalDecorators --emitDecoratorMetadata