资讯专栏INFORMATION COLUMN

巧用 TypeScript(二)

bang590 / 2073人阅读

摘要:这有一些偏方,能让你顺利从迁移至显式赋值断言修饰符,即是在类里,明确说明某些属性存在于类上采用声明合并形式,多带带定义一个,把用扩展的属性的类型,放入中是的一个提案,它主要用来在声明的时候添加和读取元数据。

Decorator

Decorator 早已不是什么新鲜事物。在 TypeScript 1.5 + 的版本中,我们可以利用内置类型 ClassDecoratorPropertyDecoratorMethodDecoratorParameterDecorator 更快书写 Decorator,如 MethodDecorator

declare type MethodDecorator = (target: Object, propertyKey: string | symbol, descriptor: TypedPropertyDescriptor) => TypedPropertyDescriptor | void;

使用时,只需在相应地方加上类型注解,匿名函数的参数类型也就会被自动推导出来了。

function methodDecorator (): MethodDecorator {
  return (target, key, descriptor) => {
    // ...
  };
}

值得一提的是,如果你在 Decorator 给目标类的 prototype 添加属性时,TypeScript 并不知道这些:

function testAble(): ClassDecorator {
  return target => {
    target.prototype.someValue = true
  }
}

@testAble()
class SomeClass {}

const someClass = new SomeClass()

someClass.someValue() // Error: Property "someValue" does not exist on type "SomeClass".

这很常见,特别是当你想用 Decorator 来扩展一个类时。

GitHub 上有一个关于此问题的 issues,直至目前,也没有一个合适的方案实现它。其主要问题在于 TypeScript 并不知道目标类是否使用了 Decorator,以及 Decorator 的名称。从这个 issues 来看,建议的解决办法是使用 Mixin:

type Constructor = new(...args: any[]) => T

// mixin 函数的声明,还需要实现
declare function mixin(...MixIns: [Constructor, Constructor]): Constructor;

class MixInClass1 {
    mixinMethod1() {}
}

class MixInClass2 {
    mixinMethod2() {}
}

class Base extends mixin(MixInClass1, MixInClass2) {
    baseMethod() { }
}

const x = new Base();

x.baseMethod(); // OK
x.mixinMethod1(); // OK
x.mixinMethod2(); // OK
x.mixinMethod3(); // Error

当把大量的 JavaScript Decorator 重构为 Mixin 时,这无疑是一件让人头大的事情。

这有一些偏方,能让你顺利从 JavaScript 迁移至 TypeScript:

显式赋值断言修饰符,即是在类里,明确说明某些属性存在于类上:

function testAble(): ClassDecorator {
  return target => {
    target.prototype.someValue = true
  }
}

@testAble()
class SomeClass {
  public someValue!: boolean;
}

const someClass = new SomeClass();
someClass.someValue // true

采用声明合并形式,多带带定义一个 interface,把用 Decorator 扩展的属性的类型,放入 interface 中:

interface SomeClass {
  someValue: boolean;
}

function testAble(): ClassDecorator {
  return target => {
    target.prototype.someValue = true
  }
}

@testAble()
class SomeClass {}

const someClass = new SomeClass();
someClass.someValue // true

Reflect Metadata

Reflect Metadata 是 ES7 的一个提案,它主要用来在声明的时候添加和读取元数据。TypeScript 在 1.5+ 的版本已经支持它,你只需要:

npm i reflect-metadata --save

tsconfig.json 里配置 emitDecoratorMetadata 选项。

它具有诸多使用场景。

获取类型信息

譬如在 vue-property-decorator 6.1 及其以下版本中,通过使用 Reflect.getMetadata API,Prop Decorator 能获取属性类型传至 Vue,简要代码如下:

function Prop(): PropertyDecorator {
  return (target, key: string) => {
    const type = Reflect.getMetadata("design:type", target, key);
    console.log(`${key} type: ${type.name}`);
    // other...
  }
}

class SomeClass {
  @Prop()
  public Aprop!: string;
};

运行代码可在控制台看到 Aprop type: string。除能获取属性类型外,通过 Reflect.getMetadata("design:paramtypes", target, key)Reflect.getMetadata("design:returntype", target, key) 可以分别获取函数参数类型和返回值类型。

自定义 metadataKey

除能获取类型信息外,常用于自定义 metadataKey,并在合适的时机获取它的值,示例如下:

function classDecorator(): ClassDecorator {
  return target => {
    // 在类上定义元数据,key 为 `classMetaData`,value 为 `a`
    Reflect.defineMetadata("classMetaData", "a", target);
  }
}

function methodDecorator(): MethodDecorator {
  return (target, key, descriptor) => {
    // 在类的原型属性 "someMethod" 上定义元数据,key 为 `methodMetaData`,value 为 `b`
    Reflect.defineMetadata("methodMetaData", "b", target, key);
  }
}

@classDecorator()
class SomeClass {

  @methodDecorator()
  someMethod() {}
};

Reflect.getMetadata("classMetaData", SomeClass);                         // "a"
Reflect.getMetadata("methodMetaData", new SomeClass(), "someMethod");    // "b"
例子 控制反转和依赖注入

在 Angular 2+ 的版本中,控制反转与依赖注入便是基于此实现,现在,我们来实现一个简单版:

type Constructor = new (...args: any[]) => T;

const Injectable = (): ClassDecorator => target => {}

class OtherService {
  a = 1
}

@Injectable()
class TestService {
  constructor(public readonly otherService: OtherService) {}

  testMethod() {
    console.log(this.otherService.a);
  }
}

const Factory = (target: Constructor): T  => {
  // 获取所有注入的服务
  const providers = Reflect.getMetadata("design:paramtypes", target); // [OtherService]
  const args = providers.map((provider: Constructor) => new provider());
  return new target(...args);
}

Factory(TestService).testMethod()   // 1
Controller 与 Get 的实现

如果你在使用 TypeScript 开发 Node 应用,相信你对 ControllerGetPOST 这些 Decorator,并不陌生:

@Controller("/test")
class SomeClass {

  @Get("/a")
  someGetMethod() {
    return "hello world";
  }

  @Post("/b")
  somePostMethod() {}
};

这些 Decorator 也是基于 Reflect Metadata 实现,不同的是,这次我们将 metadataKey 定义在 descriptorvalue 上:

const METHOD_METADATA = "method";
const PATH_METADATA = "path";

const Controller = (path: string): ClassDecorator => {
  return target => {
    Reflect.defineMetadata(PATH_METADATA, path, target);
  }
}

const createMappingDecorator = (method: string) => (path: string): MethodDecorator => {
  return (target, key, descriptor) => {
    Reflect.defineMetadata(PATH_METADATA, path, descriptor.value);
    Reflect.defineMetadata(METHOD_METADATA, method, descriptor.value);
  }
}

const Get = createMappingDecorator("GET");
const Post = createMappingDecorator("POST");

接着,创建一个函数,映射出 route

function mapRoute(instance: Object) {
  const prototype = Object.getPrototypeOf(instance);
  
  // 筛选出类的 methodName
  const methodsNames = Object.getOwnPropertyNames(prototype)
                              .filter(item => !isConstructor(item) && isFunction(prototype[item]));
  return methodsNames.map(methodName => {
    const fn = prototype[methodName];

    // 取出定义的 metadata
    const route = Reflect.getMetadata(PATH_METADATA, fn);
    const method = Reflect.getMetadata(METHOD_METADATA, fn);
    return {
      route,
      method,
      fn,
      methodName
    }
  })
};

我们可以得到一些有用的信息:

Reflect.getMetadata(PATH_METADATA, SomeClass);  // "/test"

mapRoute(new SomeClass())

/**
 * [{
 *    route: "/a",
 *    method: "GET",
 *    fn: someGetMethod() { ... },
 *    methodName: "someGetMethod"
 *  },{
 *    route: "/b",
 *    method: "POST",
 *    fn: somePostMethod() { ... },
 *    methodName: "somePostMethod"
 * }]
 * 
 */

最后,只需把 route 相关信息绑在 express 或者 koa 上就 ok 了。

至于为什么要定义在 descriptorvalue 上,我们希望 mapRoute 函数的参数是一个实例,而非 class 本身(控制反转)。

更多

巧用 TypeScript(一)

深入理解 TypeScript

文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

转载请注明本文地址:https://www.ucloud.cn/yun/98994.html

相关文章

  • 王下邀月熊_Chevalier的前端每周清单系列文章索引

    摘要:感谢王下邀月熊分享的前端每周清单,为方便大家阅读,特整理一份索引。王下邀月熊大大也于年月日整理了自己的前端每周清单系列,并以年月为单位进行分类,具体内容看这里前端每周清单年度总结与盘点。 感谢 王下邀月熊_Chevalier 分享的前端每周清单,为方便大家阅读,特整理一份索引。 王下邀月熊大大也于 2018 年 3 月 31 日整理了自己的前端每周清单系列,并以年/月为单位进行分类,具...

    2501207950 评论0 收藏0
  • 【速查手册】TypeScript 高级类型 cheat sheet

    摘要:官方文档高级类型优先阅读,建议阅读英文文档。关键字这个关键字是在版本引入的在条件类型语句中,该关键字用于替代手动获取类型。源码解释使用条件判断完成示例官方作用该类型可以获得函数的参数类型组成的元组类型。 学习 TypeScript 到一定阶段,必须要学会高阶类型的使用,否则一些复杂的场景若是用 any 类型来处理的话,也就失去了 TS 类型检查的意义。 本文罗列了 TypeScript...

    LoftySoul 评论0 收藏0
  • 每日 30 秒 ⏱ 巧用可视区域

    简介 可视区域、页面优化、DOM节点多、图片懒加载、性能 可视区域是一个前端优化经常出现的名词,不管是显示器、手机、平板它们的可视区域范围都是有限。在这个 有限可视区域 区域里做到完美显示和响应,而在这个区域外少做一些操作来减少渲染的压力、网络请求压力。在 每日 30 秒之 对海量数据进行切割 中的使用场景,我们就是利用了 有限可视区域 只渲染一部分 DOM 节点来减少页面卡顿。 既然 可视区域 ...

    DevYK 评论0 收藏0
  • 前端每周清单年度总结与盘点

    摘要:前端每周清单年度总结与盘点在过去的八个月中,我几乎只做了两件事,工作与整理前端每周清单。本文末尾我会附上清单线索来源与目前共期清单的地址,感谢每一位阅读鼓励过的朋友,希望你们能够继续支持未来的每周清单。 showImg(https://segmentfault.com/img/remote/1460000010890043); 前端每周清单年度总结与盘点 在过去的八个月中,我几乎只做了...

    jackwang 评论0 收藏0

发表评论

0条评论

最新活动
阅读需要支付1元查看
<