feat(backend): 将后端全量迁移至 TypeScript(strict) - #2
Merged
Conversation
Establish TypeScript toolchain for gradual JS→TS migration per
docs/后端TypeScript迁移实施计划.md Phase 0. No business code changed.
- Install typescript 7.0.2, tsx, @types/{node,express,jsonwebtoken,multer,cors}
- Add tsconfig.json (NodeNext, allowJs, checkJs:false, strict:false) +
tsconfig.build.json (production, excludes tests)
- package.json: add build/typecheck scripts; dev → tsx watch; start → dist/
- Add src/types/mammoth.d.ts ambient declaration (mammoth has no @types)
- vitest.config.js → vitest.config.ts (include matches .test.{js,ts})
Deviations from plan (TS 7.0 + npm registry reality):
- baseUrl removed — TS 5102: option removed in TS 7.0 (paths was empty)
- @types/uuid, @types/jwt-decode skipped — deprecated stubs, packages
ship own types
- @types/mammoth → custom ambient declaration (404 on npm registry)
- setupFiles uses concrete setup.js — vitest does not expand {js,ts}
brace glob in setupFiles (the include glob does support it)
Verified: tsc --noEmit (0 errors) · pnpm test (176/176) · pnpm build
(dist/ emitted with index.js) · tsx boots server without load errors.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Migrate all 7 pure-function files in src/utils/ from .js to .ts with type annotations. Logic unchanged; types only. - array.ts: chunk<T>, runWithConcurrency<T> generics - response.ts: ApiResponse<T> interface, typed success/error/paginated - retry.ts: RetryOptions interface, withRetry<T> generic, AxiosLikeError cast - crypto.ts: Buffer/string types, decrypt<T> generic preserves passthrough - assignee.ts: ProjectMember interface - syncCompare.ts: RemoteItem / SyncedRecord interfaces - workItem.ts: WorkloadUnit / WorkItemIds types, Map<string,string> for lookups Verified: tsc --noEmit (0 errors) · pnpm test (176/176). Confirmed vitest resolves .js import specifiers to .ts source (test files stay .js until Phase 8). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- index.ts: AppConfig interface for the exported config object, typed loadEnv/pingcodeHostAuthority - validate.ts: validateRequiredConfig(): string[] Verified: tsc --noEmit (0 errors) · pnpm test (176/176). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Migrate all 15 Sequelize model files (.js → .ts). Schema, hooks, and
associations are unchanged — verified by 176/176 tests.
Approach (deviation from plan, lower risk): keep sequelize.define() and
annotate each export as ModelDefined<Attrs, Attrs> instead of converting
to `class extends Model` + .init(). Rationale: the plan's class-based
pattern is a structural refactor that risks sync({alter:true})/hook/
association compatibility, and there is no live DB to validate against.
The define + ModelDone pattern adds full attribute typing (typed
findByPk/findAll/creation) while leaving model definitions byte-for-byte
equivalent.
- Each model exports a *Attributes interface + literal-union types for
ENUM columns (ImportRecordStatus, ImportRecordItemStatus, ModelProvider)
- User.ts / ModelConfig.ts: encrypt/decrypt hooks preserved verbatim;
hook callbacks typed (instance: any) to avoid Sequelize hook-signature
friction while keeping logic untouched
- index.ts: pure rename, associations unchanged
Verified: tsc --noEmit (0 errors) · pnpm test (176/176).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Switch all 14 models from sequelize.define() + ModelDefined<Attrs,Attrs> to `class extends Model<Attrs,Attrs>` + declare fields + .init(). Schema, hooks, associations, table names unchanged (176/176 tests green). Why: ModelDefined instances do NOT expose attributes as properties, so service/route code accessing model fields directly (config.name, user.id, record.title — pervasive) fails type-checking. The class + declare pattern provides both typed queries AND direct attribute access, matching Sequelize's official TS guidance. This is what the migration plan originally specified; the earlier ModelDefined attempt (795d97f) was a lower-risk deviation that turned out to be unworkable downstream. Verified: tsc --noEmit (0 errors) · pnpm test (176/176). statsAnalyzer (a service that reads ModelConfig fields directly) now type-checks. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Migrate all 10 service files (.js → .ts). Business logic unchanged. - db.ts: Sequelize + SeekdbClient typed; dynamic model imports resolve to index.ts; initDB/connectRelational/initDefault* return Promise<void> - agent.ts: LLMConfig interface (covers both ModelConfig instances and frontend test configs); NormalizedWorkItem output type; dynamic @langchain/anthropic import preserved - pingcode.ts: axios API wrappers typed (string token/domain params); dynamic PingCode responses kept Promise<any>; BatchCreateResult / BatchProgressItem interfaces; jwtDecode cast to Record for sub/uid - metadata.ts: MetadataCounts, PingCodeProject, Set<string> existing-id accumulators - importHelper.ts: ImportItemInput / ImportContext; reuses typed ProjectMember from utils/assignee - statsAnalyzer.ts, uploadCleanup.ts, clearSyncedData.ts, auditLog.ts, parser.ts: parameter/return types, exported option/result interfaces Verified: tsc --noEmit (0 errors) · pnpm test (176/176). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Migrate all 6 middleware files (.js → .ts). Logic unchanged. - auth.ts: JwtPayload cast, sliding-refresh logic typed; Sequelize include association (ur.Role) accessed via cast - permission.ts: requireAdmin / requirePermission typed - tokenRefresh.ts: grant-type helpers take User; ensureFreshToken typed - errorHandler.ts: HttpError interface, createError(): HttpError - logger.ts, rateLimit.ts: typed handlers / inferred limiters req.user typing — deviation from plan: the plan prescribed a global `declare module 'express-serve-static-core'` augmentation, but this project's @types/express does NOT install express-serve-static-core as a resolvable module, so a `declare module` creates an empty module that clobbers express's native Request (every property vanishes). Also, a module-form .d.ts (top-level import) is not auto-loaded by tsconfig include. Fix: a plain exported AuthedRequest interface (src/types/authRequest.ts) that extends express's Request. Under strict:false, handler param bivariance lets it register as a RequestHandler. Routes (Phase 6) will reuse AuthedRequest for req.user. Verified: tsc --noEmit (0 errors) · pnpm test (176/176). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Migrate all 12 route files (.js → .ts). Behavior unchanged; 176/176 tests green (including records/workItems whose tests mock express + workItem utils). - All authed handlers take AuthedRequest (req.user/req.isAdmin); plain routes use Request. Express 5 types req.params.* as string|string[] and req.body-sourced values surface as unknown in some positions — applied targeted `as string`/`as any[]` casts where needed. - Dynamic Sequelize where objects typed Record<string, any> / any to allow Op-prefixed (symbol) keys and literal-union value friction. - Sequelize include associations (ur.Role, rp.Permission) accessed via cast. - workItems.ts: ImportResults interface, ImportRecordStatus-typed finalStatus, Map<string,any[]> grouping; seekdb query results kept any. Supporting tweaks (routes revealed these): - utils/syncCompare: drop [key:string] index sig from RemoteItem/ SyncedRecord so SyncedProject/SyncedWorkItem class instances are structurally assignable to SyncedRecord - services/auditLog: AuditEntry/Record.detail loosened Record<…> → object (ClearSyncedResult has no index signature) - services/pingcode: BatchCreateResult.errors/created members unknown → any (flow into Sequelize where clauses) Verified: tsc --noEmit (0 errors) · pnpm test (176/176). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- app.ts: typed Express app; audit-logs/health inline routes (AuthedRequest / Request), dynamic checks object typed Record<string,string>; SPA fallback handler typed; CORS callback contextually typed - index.ts: start()/shutdown(signal) typed - prompts/*.js → *.ts via git mv (pure string exports, no content change) - package.json: dev now `tsx watch src/index.ts` - Dockerfile: install full deps (need typescript) + `pnpm run build` step; CMD → node backend/dist/index.js (src/index.js no longer exists) All backend source (.js under src/) is now TypeScript; only test files remain .js (Phase 8). Verified: tsc --noEmit (0 errors) · pnpm test (176/176) · pnpm build (dist/index.js emitted). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
git mv all 22 *.test.js → *.test.ts and setup.js → setup.ts. Test bodies unchanged (vitest globals + vi.mock factories are type-agnostic under esbuild transform). tsconfig continues to exclude **/__tests__/**, so tests are run by vitest (not type-checked by tsc) — this keeps the rich vi.mock patterns (mock express router, mock utils) working without friction. Enabling test type-checking is a possible follow-up under Phase 9 strict mode. vitest.config.ts setupFiles: setup.js → setup.ts. Entire backend (src source + tests) is now .ts. Verified: tsc --noEmit (0 errors) · pnpm test (176/176) · zero .js files under backend/src. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
tsconfig: strict:true + strictNullChecks/noImplicitAny/noUnusedLocals/ noUnusedParameters true. noImplicitReturns kept false — Express route handlers legitimately mix `return res.json()` early-exit with fall-through paths, and forcing consistency there adds churn with no behavioral value. Fixed the 28 surfaced errors (the other 40 were noImplicitReturns): - null safety: assert non-null on user.access_token/domain/pingcode_user_id in routes behind ensureFreshToken (sync/stats/workItems destructures); targetProjectId! at fetchAndStore calls; domain ?? undefined for optional pingcode domain params - unused: dropped unused Op import; _-prefixed unused req/next/params - Sequelize CreationAttributes strictness: Model.create/findOrCreate args for Role/User cast as any (auto-generated id field friction under strict) Verified: tsc --noEmit --strict (0 errors) · pnpm test (176/176) · pnpm build (dist/ emitted). Backend TypeScript migration complete: all 77 source+test files migrated, strict type-checking on, full test suite green. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
backend/src/从纯 JavaScript 全量迁移为 TypeScript(strict: true),源文件全部改为.ts,生产构建产出dist/。class extends Model+declare+.init();认证请求用显式AuthedRequest,不做 Express 全局模块增强。tsx watch src/index.ts,生产改为tsc构建后node dist/index.js;Dockerfile 同步调整。Test plan
cd backend && npx tsc --noEmit→ 0 错误cd backend && pnpm test→ 176 测试全绿cd backend && pnpm build→ 产出dist/index.jsbackend/src下无残留.js源文件pnpm dev,跑通上传 → 分析 → 匹配 → 导入