Master TypeScript Migration from Koa2 to Midway: Tame any and Build a Strong Type System
This article shares practical experiences and techniques for migrating a medium‑size Koa2 project to TypeScript with Midway, covering tsconfig settings, the dangers of any, strategies using unknown, strict compiler options, type guards, assertion functions, and how to construct a concise, reusable type system for backend models.
Recently the author migrated a medium‑size Koa2 project (using MySQL, Sequelize, request) to TypeScript and shares the lessons learned.
Project Overview
The original project had about 100 interfaces and pages. After migration it runs on Midway, MySQL, sequelize‑typescript, and axios.
TypeScript Configuration
"compilerOptions": {
"declaration": false,
"emitDecoratorMetadata": true,
"experimentalDecorators": true,
"incremental": true,
"inlineSourceMap": true,
"module": "commonjs",
"newLine": "lf",
"noFallthroughCasesInSwitch": true,
"noUnusedLocals": true,
"outDir": "dist",
"pretty": true,
"skipLibCheck": true,
"strict": true,
"strictPropertyInitialization": false,
"stripInternal": true,
"target": "ES2017"
}The article is divided into two parts: handling any and building a robust type system.
Dealing with any
Using TypeScript forces us to confront the problems caused by any. The author demonstrates why any is dangerous.
function add(a: number, b: number): number {
return a + b;
}
var a: any = '1';
var b = 2;
console.log(add(a, b)); // '12'Because a is typed as any, the compiler skips type checking, resulting in string concatenation and an unexpected '12' value that is still treated as a number downstream.
Another example shows JSON parsing returning any:
var resData = `{"a":"1","b":2}`;
function add(a: number, b: number): number { return a + b; }
var obj = JSON.parse(resData);
console.log(add(obj.a, obj.b)); // '12' JSON.parseis declared as returning any, so the type checker cannot warn that obj.a is a string.
To correct such mismatches, the author tries several approaches:
var resData = `{"a":"1","b":2}`;
function add(a: number, b: number): number {
var c = parseInt(a); // Error: Argument of type 'number' is not assignable to parameter of type 'string'.
var d: string = a as string; // Error: Conversion of type 'number' to type 'string' may be a mistake.
var e = Number(a);
return a + b;
}
var obj = JSON.parse(resData);
console.log(add(obj.a, obj.b));Only Number() works because its signature accepts any. The author questions whether this defeats the purpose of static typing.
Sources of any
Enable strict compiler options ( "strict": true) to prevent implicit any.
Inspect core libraries and third‑party typings for any usage.
Common culprits include require, JSON.parse, Koa ctx.query, and Axios generic defaults.
// Example: require returns any
interface NodeRequireFunction { (id: string): any; }
var path = require("path"); // type is any // Axios generic defaults
interface AxiosInstance {
get<T = any, R = AxiosResponse<T>>(url: string, config?: AxiosRequestConfig): Promise<R>;
}
interface AxiosResponse<T = any> { data: T; /* ... */ }Avoiding Explicit any
Do not write any yourself; use unknown as the top type introduced in TypeScript 3.0.
TypeScript 3.0 introduces a new top type unknown . unknown is the type‑safe counterpart of any . Anything is assignable to unknown , but unknown isn’t assignable to anything else without a type assertion or control‑flow narrowing.
function add(a: number, b: number): number { return a + b; }
var a: unknown = '1';
var b = 2;
console.log(add(a, b)); // Error: Argument of type 'unknown' is not assignable to parameter of type 'number'.Use type guards or assertion functions to narrow unknown:
function add(a: number, b: number): number { return a + b; }
var a: unknown = '1';
var b = 2;
if (typeof a == "number") {
console.log(add(a, b));
} else {
console.log('params error');
}Type Guards & Assertion Functions
Define custom type guards and assertion functions to validate data coming from any sources.
interface ApiCreateParams { name: string; info: string; }
function hasFieldOnBody<T extends string>(obj: unknown, names: Array<T>) : obj is { [P in T]: unknown } {
return typeof obj === "object" && obj !== null && names.every(name => name in (obj as any));
}
function assertApiCreateParams(data: unknown): asserts data is ApiCreateParams {
if (hasFieldOnBody(data, ['name', 'info']) && typeof (data as any).name === "string" && typeof (data as any).info === "string") {
console.log((data as ApiCreateParams).name, (data as ApiCreateParams).info);
} else {
throw "api create params error";
}
}
@get('/create')
async create(): Promise<void> {
let data = this.ctx.request.body; // any
assertApiCreateParams(data);
console.log(data); // now typed as ApiCreateParams
}Managing assertion functions in a dedicated file and following naming conventions makes unsafe any usage more controllable.
Do Not Use Type Assertions for any
Type assertions are a way to tell the compiler “trust me, I know what I’m doing.” They have no runtime impact and are not type‑safe.
function add(a: number, b: number) { return a + b; }
var a: any = '1';
var b = 2;
var c = a as number;
add(c, b); // prints '12'Using as on any bypasses checks and can produce incorrect results.
Overriding Third‑Party any
// Replace Koa Context query type
interface ParsedUrlQuery { [key: string]: string | string[]; }
interface IBody { [key: string]: unknown; }
interface RequestPlus extends Request { body: IBody }
interface ContextPlus extends Context { query: ParsedUrlQuery; request: RequestPlus }
@provide()
@controller('/api')
export class ApiController extends AbstractController {
@inject() ctx: ContextPlus;
}Building a Strong Type System
Large projects often need many similar types, which can violate the DRY principle. The article shows how to use TypeScript utilities to create concise, reusable types.
Interface Inheritance
interface Paging { pageIndex: number; pageSize: number; }
interface APIQueryParams extends Paging { keyword: string; title: string; }
interface PackageQueryParams extends Paging { name: string; desc: string; }Conditional Types
type Circle = { rad: number; x: number; y: number; }
type TypeName<T> = T extends { rad: number } ? Circle : unknown;
type T1 = TypeName<{ rad: number }>; // Circle
type T2 = TypeName<{ rad: string }>; // unknownUtility Types
type Serializable = { toString: (data: unknown) => string };
type Loggable = { log: (data: unknown) => void };
type A = Serializable & Loggable; // intersection
type B = Serializable | Loggable; // unionIntersection types require both contracts; union types require at least one.
Index & Mapped Types
type Person = { name: string; age: number; };
type PersonKeys = keyof Person; // 'name' | 'age'
type PersonMap = { [K in PersonKeys]: boolean }; // { name: boolean; age: boolean }Mapped types let you transform each property uniformly.
Conditional Types with infer
type Unpacked<T> =
T extends (infer U)[] ? U :
T extends (...args: any[]) => infer U ? U :
T extends Promise<infer U> ? U :
T;
type T0 = Unpacked<string>; // string
type T1 = Unpacked<string[]>; // string
type T2 = Unpacked<() => string>; // string
type T3 = Unpacked<Promise<string>>; // stringDiscriminated Unions
interface Square { kind: "square"; size: number; }
interface Rectangle { kind: "rectangle"; width: number; height: number; }
interface Circle { kind: "circle"; radius: number; }
type Shape = Square | Rectangle | Circle;
function area(s: Shape) {
switch (s.kind) {
case "square": return s.size * s.size;
case "rectangle": return s.width * s.height;
case "circle": return Math.PI * s.radius ** 2;
}
}The kind literal discriminates the union, enabling safe narrowing.
Practical Example with Sequelize‑Typescript
import { DataType, Model, Column, Comment, AutoIncrement, PrimaryKey } from 'sequelize-typescript';
const { STRING, TEXT, INTEGER, ENUM } = DataType;
export class ApiModel extends Model<ApiModel> {
@AutoIncrement @PrimaryKey @Comment("id")
@Column({ type: INTEGER({ length: 11 }), allowNull: false })
id!: number;
@Comment("parent") @Column({ type: INTEGER({ length: 11 }), allowNull: false })
parent!: number;
@Comment("name") @Column({ type: STRING(255), allowNull: false })
name!: string;
@Comment("url") @Column({ type: STRING(255), allowNull: false })
url!: string;
}Using Omit and custom Merge utility types to derive API‑specific DTOs:
type ApiObject = Omit<ApiModel, Exclude<keyof Model, "id">>; // { id:number; parent:number; name:string; url:string }
type Merge<T, S> = { [K in keyof (T & S)]: K extends keyof T ? T[K] : K extends keyof S ? S[K] : never };
type ApiCreateParams = Omit<ApiObject, "id">; // { parent:number; name:string; url:string }
type ApiQueryParams = Merge<Partial<ApiCreateParams>, Paging>; // { id?: number; parent?: number; name?: string; pageIndex:number; pageSize:number }Conclusion
TypeScript is a powerful and flexible tool whose features continue to evolve. It can be used lightly as a type‑annotation aid to catch simple errors, or more rigorously as a full type‑constraint system that requires maintaining a well‑designed type hierarchy. The author encourages developers to adopt strict compiler options, replace any with unknown, and leverage TypeScript’s advanced type utilities to keep large codebases safe and maintainable.
Reference: Node.js 项目 TypeScript 改造指南(一)
Signed-in readers can open the original source through BestHub's protected redirect.
This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactand we will review it promptly.
WecTeam
WecTeam (维C团) is the front‑end technology team of JD.com’s Jingxi business unit, focusing on front‑end engineering, web performance optimization, mini‑program and app development, serverless, multi‑platform reuse, and visual building.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.
