Backend Development 27 min read

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.

WecTeam
WecTeam
WecTeam
Master TypeScript Migration from Koa2 to Midway: Tame any and Build a Strong Type System

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

<code>"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"
}</code>

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.

<code>function add(a: number, b: number): number {
    return a + b;
}
var a: any = '1';
var b = 2;
console.log(add(a, b)); // '12'
</code>

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

:

<code>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'
</code>
JSON.parse

is 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:

<code>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));
</code>

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.

<code>// Example: require returns any
interface NodeRequireFunction { (id: string): any; }
var path = require("path"); // type is any
</code>
<code>// Axios generic defaults
interface AxiosInstance {
    get<T = any, R = AxiosResponse<T>>(url: string, config?: AxiosRequestConfig): Promise<R>;
}
interface AxiosResponse<T = any> { data: T; /* ... */ }
</code>

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.
<code>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'.
</code>

Use type guards or assertion functions to narrow

unknown

:

<code>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');
}
</code>

Type Guards & Assertion Functions

Define custom type guards and assertion functions to validate data coming from

any

sources.

<code>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
}
</code>

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.
<code>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'
</code>

Using

as

on

any

bypasses checks and can produce incorrect results.

Overriding Third‑Party any

<code>// 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;
}
</code>

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

<code>interface Paging { pageIndex: number; pageSize: number; }
interface APIQueryParams extends Paging { keyword: string; title: string; }
interface PackageQueryParams extends Paging { name: string; desc: string; }
</code>

Conditional Types

<code>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 }>; // unknown
</code>

Utility Types

<code>type Serializable = { toString: (data: unknown) => string };
type Loggable = { log: (data: unknown) => void };
type A = Serializable & Loggable; // intersection
type B = Serializable | Loggable; // union
</code>

Intersection types require both contracts; union types require at least one.

Index & Mapped Types

<code>type Person = { name: string; age: number; };
type PersonKeys = keyof Person; // 'name' | 'age'
type PersonMap = { [K in PersonKeys]: boolean }; // { name: boolean; age: boolean }
</code>

Mapped types let you transform each property uniformly.

Conditional Types with infer

<code>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>>; // string
</code>

Discriminated Unions

<code>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;
    }
}
</code>

The

kind

literal discriminates the union, enabling safe narrowing.

Practical Example with Sequelize‑Typescript

<code>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;
}
</code>

Using

Omit

and custom

Merge

utility types to derive API‑specific DTOs:

<code>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 }
</code>

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 改造指南(一)
TypeScripttype-systemKoaMidwayanyunknowntype-guards
WecTeam
Written by

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.

0 followers
Reader feedback

How this landed with the community

login Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.