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

"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.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:

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

Utility Types

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

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

Discriminated 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 改造指南(一)
Original Source

Signed-in readers can open the original source through BestHub's protected redirect.

Sign in to view source
Republication Notice

This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactadmin@besthub.devand we will review it promptly.

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

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.