标签归档:typescript

如何 parse 一个 typescript interface

blog 编程相关 Node posts 编程相关 typescript ast

目标

有如下一个接口定义,我们想把它的结构 parse 出来,知道每个字段的定义和注释,方便我们生成文档

import {IBanner} from './base/IBanner'
import {IProductGroup} from './base/IProductGroup'
/**
* 投资列表页面
*/
export interface IProductList {
/**
* 产品分组
*/
groupList: Array<IProductGroup>
/**
* 产品列表
*/
list: Array<{
a: string,
b: number
}>
/**
* 投资列表页面的banner
*/
banners: Array<IBanner>
}

typescript 提供的 ast

import * as ts from 'typescript'
const {options} = ts.convertCompilerOptionsFromJson({}, '.', 'tsconfig.json')
options.skipLibCheck = true
options.skipDefaultLibCheck = true
let program = ts.createProgram([
'./src/dashboard/interface/IProductService'
], options)
for (let sourceFile of program.getSourceFiles()) {
if (sourceFile.fileName.includes('IProductService.ts')) {
console.log('fileName', sourceFile.fileName)
console.log('statements', sourceFile.statements[1].members[])
}
}

节选一段输出:

{
pos: 38,
end: 276,
flags: 0,
transformFlags: undefined,
parent: undefined,
kind: 235,
jsDoc:
[ NodeObject {
.....
tags: undefined,
comment: '投资列表页面' } ],
decorators: undefined,
modifiers:
[ TokenObject { pos: 38, end: 64, flags: 0, parent: undefined, kind: 84 },
pos: 38,
end: 64 ],
name:
IdentifierObject {
.....
escapedText: 'IProductList' },
typeParameters: undefined,
heritageClauses: undefined,
members:
[ NodeObject {
.....
jsDoc: [Array],
modifiers: undefined,
name: [Object],
questionToken: undefined,
type: [Object] },
....

原始的 ast ,需要自己写程序遍历完整结构。

ts-simple-ast

ts-simple-ast 提供了一些元素获取的方法,例如
getInterface
getProperties

可以快速拿到你要的对象。
不足是,无法识别 import 进来的内容,例如

interfaceFile.getInterface('IBanner')

是无法获取的,因为 IBanner 是引用其他文件的

import {Project} from 'ts-simple-ast'
// initialize
const project = new Project()
project.addSourceFilesFromTsConfig('./tsconfig.json')
const interfaceFile = project.getSourceFile('src/dashboard/interface/IProductService.ts')
console.log('getProperties', interfaceFile.getInterface('IProductList').getProperties())

readts

readts 这个开源包虽然很冷门,但它却更符合我们的需求,它可以 parse 出项目里所有的 class 和 interface 等,而且连 import 进来的对象也帮你 ref 好了。

遗憾的是,匿名的对象定义还是无法 parse。

{
a: string,
b: number
}

用法:

const readts = require('readts');
const lodash = require('lodash');
var parser = new readts.Parser();
// Read configuration used in the project we want to analyze.
var config = parser.parseConfig('tsconfig.json');
// Modify configuration as needed, for example to avoid writing compiler output to disk.
config.options.noEmit = true;
// Parse the project.
var tree = parser.parse(config);
var interfaceList = lodash(tree)
.filter(item => item.interfaceList.length > 0)
.concat()
.map('interfaceList')
.map(item => item[0])
// .filter(item => item.name && item.name.match(/^I.*/))
// .value()
.find(item => item.name === 'IProductList');
console.log('interfaceList', interfaceList);

输出示例:

ClassSpec {
name: 'IProductList',
pos:
{ sourcePath: '/Users/nick/nodePro/klg-app/src/dashboard/interface/IProductService.ts',
firstLine: 6,
lastLine: 22 },
symbol:
SymbolObject {
flags: 64,
escapedName: 'IProductList',
declarations: [ [Object] ],
members:
Map {
'groupList' => [Object],
'list' => [Object],
'banners' => [Object] },
parent:
SymbolObject {
flags: 512,
escapedName: '"/Users/nick/nodePro/klg-app/src/dashboard/interface/IProductService"',
declarations: [Array],
exports: [Object],
valueDeclaration: [Object],
id: 8410 },
documentationComment: [ [Object] ],
id: 8359 },
doc: '投资列表页面',
propertyList:
[ IdentifierSpec {
name: 'groupList',
type: [Object],
optional: false,
pos: [Object],
doc: '产品分组' },
IdentifierSpec {
name: 'list',
type: [Object],
optional: false,
pos: [Object],
doc: '产品列表' },
IdentifierSpec {
name: 'banners',
type: [Object],
optional: false,
pos: [Object],
doc: '投资列表页面的banner' } ] }

Typescript 与 Mongoose 的最佳实践

前言

mongoose 是 node.js 里操作 mongodb 的最好的 orm 工具。
typescript 则是带了 type 的 js 超集。
在开发过程中经常会碰到写错字段名的问题,只有到了运行阶段才能发现(或许也没发现。。。),使用 typescript 可以达到以下目的:

  1. 智能提示,不会输错字段名啦,当然这个取决于你的 IDE 是否支持 typescript。
  2. 类型检查,错误的类型定义可以在编译期发现。

接下来就看看这两个东西怎么配合吧。

完整的 Express + Typescript + Mongoose 的 Demo 可以参考之前的文章 :使用 typescript 做后端应用开发

安装准备

  1. typescript
    typescript 的安装配置这里不赘述,可以看 typescript 的官方文档。

  2. mongoose

下载 mongoose 的类型定义文件

$ npm install mongoose --save
$ npm install @types/mongoose --save-dev
$ npm install @types/mongodb --save-dev

typescript 的 tsd 的组织方式一直在变化,目前的方式算是比较简单的,typescript 官方把常见的库 Definition 都放在 npm 上了。

Model 的定义

以一个 User Model 为例。

先定义一个接口

src/interfaces/User.ts:

export interface IUser {
createdAt?: Date;
email?: string;
firstName?: string;
lastName?: string;
}

Schema + Model

src/model/User.ts:

import {Document, Schema, Model, model} from "mongoose";
import {IUser} from "../interfaces/User";

export interface IUserModel extends IUser, Document {
fullName(): string;
}

export const UserSchema: Schema = new Schema({
createdAt: Date,
email: String,
firstName: {type: String, required: true},
lastName: {type: String, required: true}
});
UserSchema.pre("save", function (next) {
let now = new Date();
if (!this.createdAt) {
this.createdAt = now;
}
next();
});
UserSchema.methods.fullName = function (): string {
return (this.firstName.trim() + " " + this.lastName.trim());
};

export const User: Model<IUserModel> = model<IUserModel>("User", UserSchema);

Mongoose 的 tsd 中是使用泛型来实现对具体 Model 的定义的,我们自己定义的 Model 是 IUser(包含数据库字段),Mongoose 提供的基础的 Model 定义是 Document(包括 find findOne 等操作) ,继承这个两个接口就是最后的 Model 啦。

有个比较尴尬的地方是:Mongoose 的 Schema 定义和 IUser 的定义是非常相似却又不是同一个东西,你需要写两遍属性定义。而且应该会经常改了一处忘了另一处。

这里提供一个解决方案是:用代码生成器根据 Schema 来生成 Interface 。
只提供思路,不给实现。

RUN 起来

引入 User Model。
src/app.js:

import * as mongoose from 'mongoose'
// mongoose.Promise = global.Promise;
import {User} from './model/User'
mongoose.connect('mongodb://localhost:57017/test');

const db = mongoose.connection;
db.on('error', console.error.bind(console, 'connection error:'));
db.once('open', async function () {
console.log('we're connected!');
try {
await User.remove({})
const user = new User({firstName: 'feng', lastName: 'nick', email: 'jfeng@kalengo.com'})
await user.save()
const fuser = await User.findOne({})
console.log('email', user.email);
console.log('email err', user.email2);
console.log('email', fuser.email);
console.log('fullName', fuser.fullName());
console.log('lastName', fuser.lastName);
console.log('createdAt', fuser.createdAt);
} catch (err) {
console.log('err ', err.stack);
}
});

一切正常。

得到了什么

智能提示

IDE 会根据 IUser 的结构来智能提示,终于不用怕很长的字段名啦

类型检查

属性不存在

类型不匹配

OK, 这样就实现文章开头提到的目的啦。

源码

https://github.com/myfjdthink/typescript-mongoose

参考文章

TypeScript: Declaring Mongoose Schema + Model
mongoose 文档