# 前言
Typescript is a typed superset of Javascript that compiles to plain Javascript.Typescript 的静态类型检查,智能提示,IDE 友好性等特性,对于大规模企业级应用,是非常的有价值的。详见:Typescript体系调研报告 。
然而,此前使用 Typescript 开发 Egg ,会遇到一些影响开发者体验问题:
- Egg 最精髓的 Loader 自动加载机制,导致 TS 无法静态分析出部分依赖。

 - Config 自动合并机制下,如何在修改插件提供的配置时,能校验并智能提示?
 - 开发期需要独立开一个 
tsc -w独立进程来构建代码,带来临时文件位置纠结以及npm scripts复杂化。 - 单元测试,覆盖率测试,线上错误堆栈如何指向 TS 源文件,而不是编译后的 js 文件。
 
- 应用层 TS 开发规范
 - 我们在工具链方面的支持,是如何来解决上述问题,让开发者几乎无感知并保持一致性的开发体验。

 
知乎专栏的编辑体验真不咋滴,欢迎访问 Node.js 语雀专栏 ,以获取更佳的阅读体验。
# 快速入门通过骨架快速初始化:
$ npx egg-init --type=
ts showcase$ cd
 showcase &&
 npm i$ npm run dev上述骨架会生成一个极简版的示例更完整的示例参见:eggjs/examples/hackernews-async-ts# 目录规范一些约束:
- Egg 目前没有计划使用 TS 重写。
 - Egg 以及它对应的插件,会提供对应的 index.d.ts 文件方便开发者使用
 - Typescript 只是其中一种社区实践,我们通过工具链给予一定程度的支持。
 
typescript代码风格,后缀名为tstypings目录用于放置d.ts文件(大部分会自动生成)
showcase├── app│   ├── controller│   │   └── home.ts│   ├── service│   │   └── news.ts│   └── router.ts├── config│   ├── config.default.ts│   ├── config.local.ts│   ├── config.prod.ts│   └── plugin.ts├── test
│   └── ***.d.ts├── README.md├── package.json├── tsconfig.json└── tslint.json## Controller// app/controller/home.tsimport { Controller } from 'egg';export default class HomeController extends Controller {  public async index {const { ctx, service } = this;const page = ctx.query.page;const result = await service.news.list;await ctx.render;  }}## Router// app/router.ts
import
 {
 Application
 }
 from
 'egg'
;
export
 default
  =>
 {
  const
 {
 router
,
 controller
 }
 =
 app
;
  router
.
get
;
};
## Service// app/service/news.ts
import
 {
 Service
 }
 from
 'egg'
;
export
 default
 class
 NewsService
 extends
 Service
 {
  public
 async
 list
:
 Promise
<
NewsItem
[]
>
 {
return
 [];
  }
}
export
 interface
 NewsItem
 {
  id
: number
;
  title
: string
;
}
## Middleware// app/middleware/robot.tsimport { Context } from 'egg';export default function robotMiddleware {  return async  => {await next;  };}因为 Middleware 定义是支持入参,第一个参数为同名的 Config,如有需求,可以用完整版:// app/middleware/news.tsimport { Context, Application } from 'egg';import { BizConfig } from '../../config/config.default';// 注意,这里必须要用 ['news'] 而不能用 .news,因为 BizConfig 是 type,不是实例export default function newsMiddleware {  return async  => Promise) => {console.info;await next;  };}## Extend// app/extend/context.tsimport { Context } from 'egg';export default {  isAjax {return this.get === 'XMLHttpRequest';  },}// app.tsexport default app => {  app.beforeStart => {await Promise.resolve;  });};## ConfigConfig 这块稍微有点复杂,因为要支持:- 在 Controller,Service 那边使用配置,需支持多级提示,并自动关联。
 - Config 内部, 
config.view = {}的写法,也应该支持提示。 - 在 
config.{env}.ts里可以用到config.default.ts自定义配置的提示。
 
// app/config/config.default.tsimport { EggAppInfo, EggAppConfig, PowerPartial } from 'egg';// 提供给 config.{env}.ts 使用export type DefaultConfig = PowerPartialnfig & BizConfig>;// 应用本身的配置 Schemeexport interface BizConfig {  news: {pageSize: number;serverUrl: string;  };}export default  => {  const config = {} as PowerPartial & BizConfig;  // 覆盖框架,插件的配置  config.keys = appInfo.name + '123456';  config.view = {defaultViewEngine: 'nunjucks',mapping: {  '.tpl': 'nunjucks',},  };  // 应用本身的配置  config.news = {pageSize: 30,serverUrl: 'https://hacker-news.firebaseio.com/v0',  };  return config;};  简单版:// app/config/config.local.tsimport { DefaultConfig } from './config.default';export default  => {  const config: DefaultConfig = {};  config.news = {pageSize: 20,  };  return config;};备注:- TS 的 
Conditional Types是我们能完美解决 Config 提示的关键。 - 有兴趣的可以看下 egg/index.d.ts 里面的 
PowerPartial实现。 
// {egg}/index.d.tstype PowerPartial = {  [U in keyof T] : T[U] extends {}  PowerPartial: T[U]};  ## Plugin// config/plugin.tsimport { EggPlugin } from 'egg';const plugin: EggPlugin = {  static: true,  nunjucks: {enable: true,package: 'egg-view-nunjucks',  },};export default plugin;## Typings该目录为 TS 的规范,在里面的 ***.map":
 true
,
// 光注释掉 when 这行无效,需全部干掉
// "**/*.js": {
//  "when": "$.ts"
// }
  },
  "typescript.tsdk"
:
 "node_modules/typescript/lib"
}
## package.json完整的配置如下:{
  "name"
:
 "hackernews-async-ts"
,
  "version"
:
 "1.0.0"
,
  "description"
:
 "hackernews showcase using typescript && egg"
,
  "private"
:
 true
,
  "egg"
:
 {
"typescript"
:
 true
  },
  "scripts"
:
 {
"start"
:
 "egg-scripts start --
,
"stop"
:
 "egg-scripts stop --
,
"dev"
:
 "egg-bin dev -r egg-ts-helper/register"
,
"debug"
:
 "egg-bin debug -r egg-ts-helper/register"
,
"test-local"
:
 "egg-bin test -r egg-ts-helper/register"
,
"test"
:
 "npm run lint -- --fix && npm run test-local"
,
"cov"
:
 "egg-bin cov -r egg-ts-helper/register"
,
"tsc"
:
 "ets && tsc -p tsconfig.json"
,
"ci"
:
 "npm run lint && npm run tsc && egg-bin cov --no-ts"
,
"autod"
:
 "autod"
,
"lint"
:
 "tslint ."
,
"clean"
:
 "ets clean"
  },
  "dependencies"
:
 {
"egg"
:
 "^2.6.0"
,
"egg-scripts"
:
 "^2.6.0"
  },
  "devDependencies"
:
 {
"@types/mocha"
:
 "^2.2.40"
,
"@types/node"
:
 "^7.0.12"
,
"@types/supertest"
:
 "^2.0.0"
,
"autod"
:
 "^3.0.1"
,
"autod-egg"
:
 "^1.1.0"
,
"egg-bin"
:
 "^4.6.3"
,
"egg-mock"
:
 "^3.16.0"
,
"egg-ts-helper"
:
 "^1.5.0"
,
"tslib"
:
 "^1.9.0"
,
"tslint"
:
 "^4.0.0"
,
"typescript"
:
 "^2.8.1"
  },
  "engines"
:
 {
"node"
:
 ">=8.9.0"
  }
}
# 高级用法## 装饰器通过 TS 的装饰器,可以实现 依赖注入 / 参数校验  / 日志前置处理 等。import { Controller } from 'egg';export default class NewsController extends Controller {  @GET  public async detail {const { ctx, service } = this;const id = ctx.params.id;const result = await service.news.get;await ctx.render;  }}目前装饰器属于锦上添花,因为暂不做约定。交给开发者自行实践,期望能看到社区优秀实践反馈,也可以参考下:egg-di 。友情提示:要适度,不要滥用。## tegg未来可能还会封装一个上层框架 tegg,具体 RFC 还没出,还在孕育中,敬请期待。名字典故:
typescript + egg -> ts-egg -> tea egg -> 茶叶蛋 Logo:见题图# 写在最后早在一年多前,阿里内部就有很多 BU 在实践 TS + Egg 了。随着 TS 的完善,终于能完美解决我们的开发者体验问题,也因此才有了本文。
本来以为只需要 2 个 PR 搞定的,结果变为 Hail Hydra,好长的 List:
终于完成了 Egg 2.0 发布时的一大承诺,希望能通过这套最佳实践规范,提升社区开发者的研发体验。免责声明:本平台仅供信息发布交流之途,请谨慎判断信息真伪。如遇虚假诈骗信息,请立即举报
举报













