Skip to content

t1k:cocos:base:js2ts

FieldValue
Modulebase
Version1.14.0
Efforthigh
Tools

Keywords: ccclass, cocos, codemod, commonjs, decorator, js2ts, migration, ts-morph, typescript

/t1k:cocos:base:js2ts

Activate to convert a Cocos 2.4.15 .js component to a dormant .cv.ts during the JS→TS migration, AFTER t1k-cocos-base-dep-graph has produced the Export-Form Registry. This is the js2ts step in the pipeline dep-graph → scope-audit(read) → js2ts → tsc-validate → cutover → uuid-verify.

Mission (inherited, absolute): convert SYNTAX only, keep 100% behavior, app runs identically. The codemod copies method bodies verbatim and only adds type-level declarations needed to compile (behavior-preserving). It does NOT fix bugs, scope-bugs, or logic.

Do NOT use for: deleting/renaming the .js (that is t1k-cocos-cutover), uuid transplant, or scene/prefab edits.

Node.js CLI scripts/js2ts.cjs (ts-morph):

  1. Reads the registry entry for the target file (--registry dep-registry.json): uuid, exportForm, valueRequired, tsExport, componentName.
  2. Parses the .js as an AST (ts-morph), locates the single cc.Class({...}) call and classifies every top-level statement.
  3. Emits a decorator class matching the project idiom (const { ccclass, property } = cc._decorator;@ccclass('<name>') → class).
  4. Surgical type-resolve pass: writes the file, runs the TS checker with creator.d.ts, collects TS2339/TS2551 (Property 'X' does not exist / same with a “did you mean” hint) errors whose access is on this, and declares each as X: any; (no initializer → no runtime emit at target es5). Base-provided members (cc.Component.node, getComponent, …) never raise the error, so they are never shadowed.
  5. Writes <file>.cv.ts (with --write) or prints to stdout (dry-run).

The source .js is read-only — never written.

The TS export form is taken from registry[file].tsExport (computed usage-aware by dep-graph):

exportFormtsExportModeEmitted shapeStatus
cc-componentexport defaultA@ccclass('X_cv') export default class X_cv extends Base {}✅ tsc
cc-component / commonjs-classexport =A@ccclass('X_cv') class X_cv extends Base {} export = X_cv;✅ tsc
commonjs-objectexport =Bbody verbatim; module.exports = Eexport = E✅ tsc
none (side-effect)export {}Bbody verbatim; requires→import; append export {}; + globals.d.ts + cc-ext.d.ts✅ tsc
es-default / es-namedexport default / namedCbody verbatim; relative import/export … from.cv✅ tsc

Three modes — routed by exportForm (the registry SSOT), NOT by cc.Class presence. Mode A (cc-component / commonjs-class) rebuilds a decorator class. Mode B (commonjs-object / none / commonjs-named) keeps the ENTIRE file verbatim (incl. comments) and only rewrites top-level var X = require()import X = require(), module.exports = Eexport = E, and appends export {}; for a none file. Mode C (es-default / es-named) is for a file that is ALREADY an ES module (import/export, often ES2022 class fields) — keep the body VERBATIM and only rewrite RELATIVE import/export … from specifiers to the .cv coexistence sibling (if it exists). No export conversion (ES exports ARE TS exports), no export {} append. Simplest of the three. Maximum integrity — no body restructuring. A none file may still CONTAIN a cc.Class — an engine PATCH like cc.Update = cc.Class({ name: "cc.Update", ... }) (e.g. hack.js) — which must stay VERBATIM (Mode B), NOT be rebuilt as a component. Routing by cc.Class-presence (the old bug) sent such files to Mode A → threw on the name/other options.

Mode A preserves side-effect top-level statements. A cc.Class file often has other top-level statements beside the class (window.BULLET_FADE_TYPE = {...}, var X;, …). These are kept VERBATIM in source order (before/after the class) — they run at module load exactly as before; the bare globals they assign rely on globals.d.ts. Structural coverage: 307/307 cc.Class files convert without throwing (0 holdout). Hai holdout cuối đã giải bằng cách KHÔNG-đụng-dep-graph: Item.js (wrapped properties + computed-key) → unwrap + resolve CONST+"Icon" qua constantsFile (xem Property transform); EffectManager.js (2 cc.Class patch+bare, dep-graph misclassify) → exportFormOverride ép none = Mode B verbatim (file thực ra là side-effect module). → mọi scene game (main 24 / login 26 / backpack 50) Gate 2 PASS.

cc.Class options (Mode A). Handled: extends → heritage clause (or none); properties@property fields only when the class is a true component (registry[file].isComponent — transitively extends cc.Component; a no-extends cc.Class like Proto OR one extending a non-component like Entity extends Proto → plain fields/accessors, else TS1240 — see Gotcha 12), and computed {get,set} entries → TS accessors (see Property transform); statics: {X:v, fn(){}, get g(){}, set s(v){}}static X = v; static fn(){} static get g(){} static set s(v){} (get/set accessor support; setter param KHÔNG optionalize, TS1051 cấm set x(v?)); editor: {...} → class decorators; ctor: function(){}constructor(){ [super();] ... }; name: "X" → reserved CLASS-NAME key, DROPPED (@ccclass(componentName) từ registry SSOT đã mang tên); top-level NON-function data field (ngoài properties, vd data: null, itemList: void 0) → plain instance default name: any = value; (KHÔNG @property — behavior-preserving). mixins → throws (0 in this project). Destructuring require (const { default: X } = require("Y")) → giữ verbatim const {…} = require("Y.cv") (import=require KHÔNG destructure được; require runtime Cocos thật, type residual any-safe). Wrapped properties + computed-key (vd Item.js: properties: ((a={…}),(a[CONST+"Icon"]=v),…,a)) → unwrap sequence + resolve CONST+"Icon" thành tên literal (blueprintAtkIcon) qua constantsFile map → build synthetic object literal → convertProperties SẴN CÓ (verify: attach completeness 29/29 khớp serialized key trên 6 prefab). Alias rename: var i = cc.Class({...}) — obfuscated class-var i renamed (scope-correct) to TS class name.

Config override (cocos-migrate.json). exportFormOverride: { "<file>": "none" } — ép exportForm cho file dep-graph phân loại SAI, KHÔNG đụng dep-graph algorithm (rủi ro cả 434). Vd EffectManager.js: dep-graph thấy cc.Recycle = cc.Class(...) (engine-patch) → cc-component, nhưng file là side-effect module (bare anonymous cc.Class không name/extends/scene-ref) → ép none = Mode B verbatim. constantsFile: "<path>" — file global string-const (IDENT="lit" hoặc window.IDENT="lit") để resolve computed-key properties.

To let the dormant TS validate in parallel with the live JS (brainstorm §3/§4):

  • File: Foo.jsFoo.cv.ts (basename Foo.cv, so it never collides with the live Foo.js).
  • Class + @ccclass: @ccclass('Foo_cv') + class Foo_cv — a temp registered name so Cocos sees no duplicate of the live Foo. The class IDENTIFIER is sanitized (e.g. map-scroll-viewmap_scroll_view_cv); the @ccclass STRING keeps the original chars + _cv.
  • Internal imports point at .cv: a relative require("./Dep") becomes import Dep = require("./Dep.cv") iff Dep.cv.ts already exists on disk. This makes the converted set a self-consistent, type-checkable module graph (all real export =/export default modules) while the live .js set runs the game. At cutover the .cv suffix is stripped (→ Foo.ts, @ccclass('Foo'), require('./Dep')).

Convert in dependency order (base/dep before dependents) so the .cv sibling exists when a dependent is converted; otherwise the import stays ./Dep and won’t type-check against the bare-cc.Class .js (see Gotcha 2).

properties: { ... } entries → @property fields:

JS formTS emitted
content: cc.Sprite (shorthand type)@property(cc.Sprite)
content: cc.Sprite = null;
list: [cc.Node] (array shorthand)@property([cc.Node])
list: cc.Node[] = [];
{ type: cc.Integer, default: 5, tooltip: 't' }@property({ type: cc.Integer, tooltip: 't' })
name: number = 5;
count: 5 / flag: false / label: "" (literal default)@property
count: number = 5; (type inferred)
x: { get: function(){…}, set: function(e){…} } (computed)get x() {…}
set x(e) {…} (TS accessor, body VERBATIM — NOT @property)

cc.Integer/cc.Floatnumber, cc.Stringstring, cc.Booleanboolean, cc.X/sp.X/require’d-class→same, else any.

Computed (get/set) properties → TS accessors, not @property. A cc.Class { get, set } property IS an accessor; emitting @property({ get, set }) is invalid (TS2353 “‘get’ does not exist in the descriptor type”). The codemod emits a real get name(){…} / set name(e){…} (works on any class, component or not; behavior-preserving — accessor defined on the prototype exactly as cc.Class did). The @property vs plain-field/accessor decision is gated by isComponent (Gotcha 12).

Bare window-globals — generated globals.d.ts

Section titled “Bare window-globals — generated globals.d.ts”

Every project global is a runtime window.X = ... (e.g. window.EFFECT = {} in EffectManager.js, window.ZORDER = {...} in define.js, window.kit = {} in kit.js) accessed bare (EFFECT.x, kit.clamp). Once a file becomes a TS module, a bare name needs an ambient declare var X: any; — a interface Window augmentation (the existing types/GameConfigTypes.d.ts pattern) only types window.X, NOT bare X.

scripts/gen-globals-dts.cjs generates an ambient globals.d.ts (SSOT = the dep-graph registry):

  • declares every _meta.globalProvidedBy name + non-leak _meta.externalGlobals (SDK/UPPER_CASE/camelCase, now including mini-game platform globals tt/wx/… that dep-graph no longer filters) + declare function require. (No hardcoded platform allowlist — they arrive via externalGlobals from the registry, SSOT.)
  • drops leak-like names (single lowercase letter e/a/i/n/…, plus rnd_name/rnd_head) so they surface as tsc errors for a later scope fix (policy: user decision; aligns with cocos2x-scope-audit). 2-char names are KEPT (many are real SDKs: qg=quickgame, ks=kuaishou, uc, bl=bilibili).
Terminal window
node .claude/skills/t1k-cocos-base-js2ts/scripts/gen-globals-dts.cjs --registry tmp/dep-registry.json --out Client/assets/script/types/globals.d.ts

Regenerate after any dep-graph re-run. Include globals.d.ts in the validation tsconfig.

The game patches the cc namespace at runtime (cc.recycle = ...) AND uses cc members absent from this project’s (partial) creator.d.ts (cc.vec4(...), cc.js, cc.gfx, cc.RotateBy …) → TS2339. scripts/gen-cc-ext-dts.cjs scans the source for every cc.<id> member access (assignments + read/call usage), filters out members already declared in creator.d.ts (avoids duplicate-identifier; legit cc.Node/cc.Sprite/cc.log never re-declared), and emits a declare namespace cc { let <id>: any; } augmentation (type-only, 0 runtime emit → behavior-preserving). This project: 12 custom members (recycle, msgbox, dump, vec4, js, gfx, RotateBy/RotateTo, …); cc.update is engine-owned so it is skipped.

Terminal window
node .claude/skills/t1k-cocos-base-js2ts/scripts/gen-cc-ext-dts.cjs --root Client/assets/script --types Client/creator.d.ts --out Client/assets/script/types/cc-ext.d.ts

let (not const/function) so both the patch assignment cc.recycle = fn and every consumer cc.recycle() type-check. Caveat: a file that reassigns an engine-owned member (cc.update = ..., which creator.d.ts declares as a function) still raises an assignment error — that is a genuine override the codemod surfaces, not something cc-ext.d.ts masks.

Custom node members — generated node-ext.d.ts

Section titled “Custom node members — generated node-ext.d.ts”

The game stores custom props on node INSTANCES (this.node.onTouch = fn, then var e = this.node.onTouch). this.node is cc.Node, which has no onTouch → TS2339 (4th dynamic category, distinct from bare globals / cc-namespace patches / this.X). scripts/gen-node-ext-dts.cjs scans the source for .node.<id> accesses, filters out every real cc.Node member (incl. inherited from _BaseNode, read via the ts-morph type checker — avoids the interface/class merge error on active/x/on/…), and emits declare namespace cc { interface Node { <id>: any; } } (interface-merges with the engine’s cc.Node class; type-only, 0 runtime emit).

Terminal window
node .claude/skills/t1k-cocos-base-js2ts/scripts/gen-node-ext-dts.cjs --root Client/assets/script --types Client/creator.d.ts --out Client/assets/script/types/node-ext.d.ts

This project: 18 custom members (onTouch, runActionVS, recycleFunc, isDestroyed, …). Scope: only the .node.<id> receiver is detected; custom members reached via an aliased node var or event.target.<id> are not, and surface as TS2339 for a follow-up.

Nested / prototype engine patches — generated engine-ext.d.ts

Section titled “Nested / prototype engine patches — generated engine-ext.d.ts”

cc-ext (cc.<id>) and node-ext (this.node.<id>) each own ONE receiver shape. An engine-bootstrap file (hack.js) patches engine internals at OTHER receivers that neither covers: cc.sys.<id> (custom langs), cc.director.<id> (_scheduler), cc.Node/Component/Action.prototype.<id> (incl. var h = cc.Node.prototype; h.<id> = …), and the foreign dragonBones.<Class>.prototype.<id> / dragonBones.WorldClock.<staticId>. Each maps to a different augmentation (namespace member vs class interface-merge). scripts/gen-engine-ext-dts.cjs is config-driven — the RECEIVERS table maps each { receiver, ns, target, kind: namespace|interface, alias }:

  • scans each receiver (direct <recv>.<id> + alias var h = <recv>; h.<id> = … for prototype patches);
  • filters against creator.d.ts — namespace vars/functions/enums + class STATIC and INSTANCE members incl. inherited (so real members are never re-declared → no duplicate-identifier). cc.sys/dragonBones.WorldClock are classes accessed as typeof X, so their statics (OS_IOS, …) are filtered via the static side. A namespace declared across MULTIPLE declare namespace <ns> { … } blocks (dragonBones has ~10) is searched across ALL blocks (getModules().filter), else Slot/WorldClock are missed;
  • filters against node-ext.d.ts (so cc.Node members aren’t double-declared);
  • emits grouped by namespace: declare namespace cc { namespace sys { let X } interface Director/Node/Component/Action { X } } + declare namespace dragonBones { interface Slot/ArmatureDisplay { X } namespace WorldClock { let X } }. let (not const) for namespace members — the game ASSIGNS them (cc.sys.LANGUAGE_VIETNAMESE = "vi" → a const raises TS2540).
Terminal window
node .claude/skills/t1k-cocos-base-js2ts/scripts/gen-engine-ext-dts.cjs --root Client/assets/script --types Client/creator.d.ts --out Client/assets/script/types/engine-ext.d.ts --node-ext Client/assets/script/types/node-ext.d.ts

This project: 29 members across cc.{sys,Director,Node,Component,Action} + dragonBones.{Slot,ArmatureDisplay,WorldClock}. Cleared 36/51 hack.cv.ts errors. Remaining 15 are NOT receiver-typing: 6 function-object property patches (var f = …; f.<id> = … — the receiver is a function value; intractable) + 9 verbatim obfuscated-code quirks (!1 & bool TS2447, arity TS2554, non-callable TS2349, … — the same accepted-residual category as every file, behavior-preserving). Include engine-ext.d.ts in the validation tsconfig alongside the other three.

Config — .claude/cocos-migrate.json (SSOT, KHÔNG hardcode)

Section titled “Config — .claude/cocos-migrate.json (SSOT, KHÔNG hardcode)”

--registry (key registry) và --types (key creatorDts) mặc định lấy từ .claude/cocos-migrate.json (dùng chung với t1k-cocos-base-uuid-verify/t1k-cocos-base-tsc-validate/t1k-cocos-base-migrate). Có config → gọi js2ts KHÔNG cần truyền 2 flag đó. Thứ tự: CLI flag > config > fallback (registry=null bắt buộc, types=auto walk-up). Xem t1k-cocos-base-migrate § Config.

Terminal window
# Có .claude/cocos-migrate.json → không cần --registry/--types:
node .claude/skills/t1k-cocos-base-js2ts/scripts/js2ts.cjs <file.js> # dry → stdout
node .claude/skills/t1k-cocos-base-js2ts/scripts/js2ts.cjs <file.js> --write # ghi <file>.cv.ts
# Dry-run (prints TS to stdout; surgical pass uses a temp sibling, then deletes it):
node .claude/skills/t1k-cocos-base-js2ts/scripts/js2ts.cjs <file.js> --registry tmp/dep-registry.json
# Write <file>.cv.ts:
node .claude/skills/t1k-cocos-base-js2ts/scripts/js2ts.cjs <file.js> --registry tmp/dep-registry.json --write
# Override creator.d.ts location (else auto-located by walking up):
... --types Client/creator.d.ts
# Skip the surgical type-resolve pass (faster; may leave TS2339):
... --no-resolve

Validate the produced .cv.ts set with tsc --noEmit against creator.d.ts (module commonjs, target es5, experimentalDecorators, skipLibCheck, allowJs, noImplicitAny off — matches Client/tsconfig.json, the real Cocos 2.4.15 project config which sets NO strict/noImplicitAny → defaults to off).

noImplicitAny: false is REQUIRED, not optional. Params are emitted as bare e? (optional, type implicitly any); the codemod does NOT write : any (it would be redundant under the project config). Under noImplicitAny: true / strict (an IDE-only setting — the project tsconfig does NOT enable it), every un-annotated param raises TS7006. On LevelManager.cv.ts: 0 under the project config vs 141 under strict — of which 108 are emitted method params (fixable with e?: any) but 33 are params of top-level helpers / nested callbacks inside VERBATIM bodies, unfixable without editing the body (Gotcha 4). So strict-clean is unreachable for obfuscated code; the migration targets noImplicitAny: false (decision, user). If an IDE shows TS7006, point it at Client/tsconfig.json rather than annotating.

  1. Dynamic this.X (base→subclass method, prototype patch) needs a declaration, NOT a body edit. Real example: Transition.js (base) calls this.enterTransition() which only TransitionGo.js (a sibling subclass) defines — a valid polymorphic pattern, not a bug. The surgical pass declares enterTransition: any; on the base class. Decision (user): surgical per-member X: any;, chosen over a blanket [key:string]: any index signature (keeps type-checking everywhere else) and over // @ts-nocheck (which kills all checking). A bare X: any; (no initializer) emits no runtime code at es5 → behavior-preserving.

  2. A bare-cc.Class .js is “not a module” to TS (TS2306). A cc-component .js has no module.exports (Cocos auto-registers it), so import X = require("./Dep") against the live Dep.js fails type-check. Fix = the .cv coexistence import (point at the converted Dep.cv.ts, a real module). Consequence: convert in dependency order, and a not-yet-converted relative dep will not type-check until it is converted.

  3. this.node falsely declared if the typed base is lost. If the import resolves to a non-module/any base, every this.member raises 2339 and gets a spurious any decl (observed: node declared on TransitionCover before the .cv import fix). Always resolve the import to the typed .cv base so inherited members (node, etc.) stay typed and are not redeclared.

  4. Method bodies are copied VERBATIM (getBody().getText()), only re-indented as a block. No statement is rewritten — integrity. Obfuscated single-letter params (e, t, i) are preserved untyped (tsconfig has noImplicitAny off → compiles).

  5. Loud-fail on unhandled cc.Class options. ctor, editor, statics, mixins, non-function data fields, multiple cc.Class() calls, and unrecognized top-level statements throw rather than convert lossily. Add explicit support before converting such files (integrity > silent drop).

  6. export = + decorator class compiles under module: commonjs + experimentalDecorators (verified). The class is declared then export = X; (decorator on the non-exported declaration).

  7. cc.X engine patches need a declare namespace cc augmentation — done via the generated cc-ext.d.ts (see section above). The game patches cc.recycle/cc.msgbox/cc.dump/cc.Recycle/cc.Update; globals.d.ts (bare globals) does NOT cover these (they’re cc. members, not bare). With cc-ext.d.ts included, bonusEffect.cv.ts validates EXIT=0.

  8. this._supersuper.<methodName> transform (DONE, on by default). cc.Class injects this._super to call the same-named superclass method (70 files / 105 refs in this project); an ES extends class has no _super. The codemod replaces every this._super PropertyAccess with super.<enclosing-method-name>, which handles all call shapes uniformly (this._super(a)super.foo(a), this._super.apply(this,arguments)super.foo.apply(this,arguments)). This is the ONE body-editing transform — a behavior-PRESERVING syntax conversion (without it this._super is undefined → runtime crash). Proven: RewardBtn.onLoad this._super()super.onLoad() resolves against the converted base. Disable with --no-super-rewrite (then _super stays verbatim + gets declared — compiles but runtime-wrong). Caveat: super.foo() requires the typed base (.cv) to declare foo; convert in dependency order. A this._super inside a nested non-arrow function(){} (where this is rebound — a pre-existing bug) would become an invalid super — rare; tsc flags it.

9b. No-extends cc.Class must NOT default to extends cc.Component. A bare cc.Class({...}) with no extends key is a plain base class (e.g. Proto), NOT a component — the codemod emits class X {} with no heritage clause. (Bug fixed: previously injected a spurious cc.Component base.)

  1. Dynamic members on cc.Node instances — done via the generated node-ext.d.ts (see section above). The game stores custom props on nodes (this.node.onTouch); cc.Node has no onTouch → TS2339 (4th dynamic category, distinct from bare-globals / cc-namespace patches / this.X). node-ext.d.ts interface-merges the custom members onto cc.Node. With it included, ClickButton.cv.ts validates EXIT=0. Remaining gap: custom members reached via an aliased node var or event.target.<id> (not .node.<id>) are still uncovered.

  2. Obfuscated comma-wrapper arg — handled (Bullet/Merchant). cc.Class( ((i = {OBJ}).onHitWall = fn, i.remove = fn, i.statics = {…}, i) ): the config is assigned to a local i (the base i = {OBJ} is often NESTED in the first attachment’s LHS (i = {OBJ}).key), members are attached as i.<key> = <val> (functions → instance methods, statics → static block), and i is returned (often via a result var var a = cc.Class(...)). The codemod: unwraps the comma-sequence (unwrapWrapperArg), finds the nested base object, collects attachments, renames BOTH the config alias i AND the result var a → the TS class (statics/methods reference the class through them: a.shot(…)ClassName.shot(…)), and dedupes an attachment method that overrides an inline method of the same name (i.remove = fn wins → no TS2393 duplicate). Validated: Bullet.cv.ts — all residual errors are the #7 method-variance category + intentional single-letter-leak TS2304, none are wrapper bugs. Still NOT handled: Item.js (two cc.Class() calls + a properties: ((a = {…}), …) wrapped-properties form).

  3. @property only on a TRUE component (transitive isComponent). @property is valid ONLY when the class transitively extends cc.Component (proven by isolation test: @property on a class extending cc.Component compiles; on a plain class → TS1240 “Unable to resolve signature of property decorator”). The codemod reads registry[file].isComponent (dep-graph computes it by walking the extends chain to a cc.* engine component base in ENGINE_COMPONENT_BASES); non-components emit properties as plain fields / accessors. This REPLACES the old “has a base → @property” heuristic, which was wrong: Entity extends Proto (Proto = a no-extends plain base, NOT a component) → Entity is NOT a component, but the old heuristic decorated it → 9× TS1240. Now isComponent(Entity)=false (Entity→Proto→∅) → plain fields, 0 TS1240. Impact: the whole Proto/Entity subtree (≈36 files: AI behaviors, Hero, Vehicle, Drop, …) is non-component and correctly un-decorated. A real component (extends cc.Component/cc.Sprite/…) still gets @property (e.g. buffBannerEffect).

  4. Method arity variance — handled via a TARGETED type-only OVERLOAD (NOT a rest param). Obfuscated JS treats every parameter as optional and ignores extra args; TS enforces arity → over-call (TS2554, called with MORE args than declared) and under-call / derived-adds-param (TS2554/TS2416). Under-call & derived-adds-param are resolved by optionalizeParam (each simple param → optional). For over-call, the codemod emits a type-only overload signature name(...args: any[]): any; immediately above the method/static (overloadLine); it accepts any argument count, is ERASED at emit (0 runtime code), and the impl keeps its original params. Targeting (full call-graph): the overload is emitted ONLY when the method is actually over-called somewhere — dep-graph now ships a whole-program methodArity map (NAME → max args any call passes; keyed by name, a safe over-approximation) and overloadLine emits the overload iff methodArity[name] > declaredParamCount. So getConfig/continue/send (over-called) get one; onExit()/clear() (never over-called) stay clean. On LevelManager.js this cut overloads 73 → 7. Constructors are always overloaded (called via import-aliased new Alias(...), so arity-by-name is unreliable; only a handful project-wide). Safety: a missed over-call (e.g. via dynamic obj[x](...) dispatch the map can’t see) degrades to a visible TS2554 residual at validation — NEVER a runtime change — because the overload is purely a type-level aid. Why not a rest param on the impl: at target es5 a rest param emits an arguments-gather loop (var args = []; for (var _i …)) EVEN WHEN UNUSED — a real per-call array allocation, NOT behavior-preserving. This corrects the prior 0-param → ...args stub approach, which DID inject that loop (the noEmit: true validation config hid it). Proven on LevelManager.js (1747 lines): every class/static over-call TS2554 cleared; the emitted JS contains 0 gather-loops and 0 leaked overloads, impls emit with original params (LevelManager_cv.getConfig = function (e, t) {…}). Validated: Buff/Proto (override chain) + async/generator/static/ctor edge cases EXIT=0. Residual NOT covered (by design): a LOCAL function-expression inside a verbatim method body (var n = function (t, i){…} then n(x)) — its arity mismatch surfaces as TS2554, but fixing it would require editing the verbatim body (forbidden by Gotcha 4), so it is left as a classified residual (behavior-irrelevant: the source JS ignored the arg-count too). Also residual: a few TS2353 (object-literal excess-property, narrowly-inferred param shape).

  5. Surgical pass over-declares when the base dep is unresolved. If extends Base resolves to a not-yet-converted bare-cc.Class .js (an any/non-module base), EVERY this.member raises 2339 and gets a spurious any field. Convert in dependency order so the typed .cv base exists first; then only genuinely-undeclared members are declared.

tsc-validated: cc-component (export default + export =: buffBannerEffect, Transition, TransitionCover), commonjs-object (VirtualAvatar, LeaderboardConstants), none/side-effect (bonusEffect — modulo cc.X patches). commonjs-class (BulletLaser) converts structurally (Mode A).

tsc-validated additions: _supersuper transform (RewardBtn/ClickButton chain) + no-extends fix.

tsc-validated additions: editor→decorators (FullScreenWidget), ctor→constructor (ColorMatrix), statics+alias-rename (Buff), @property-only-on-component fix (Proto).

tsc-validated additions: uniform type-only overload for arity tolerance (LevelManager — 1747-line cc-component, no-extends base; every class/static over-call TS2554 cleared, emit verified 0 gather-loops). Replaces the rest-param stub (fixes the latent es5 arguments-gather emit). Remaining LevelManager residuals are all classified: dep-order new $drop() (TS2351, clears when Drop is converted), local-function-expr arity (TS2554, Gotcha 13 residual), scope-hazard single-letter shadow-mutation (TS2367/TS2322 → cocos2x-scope-audit).

Not yet built:

  • TS2353 object-literal excess-property (narrowly-inferred param shape) — minor, unrelated to param-variance.
  • ĐÃ FIX HẾT 5 holdout → 307/307 convert, 0 throw:
    • LeaderboardData: statics get/set accessor + destructuring require (const {default:X}=require() verbatim) + setter param không-optionalize (TS1051).
    • ItemAdvanced/DraftMenu: data field → plain instance field; name = reserved class-name key → drop.
    • Item: wrapped-properties + computed-key → unwrap + resolve CONST+"Icon" qua constantsFile (attach completeness 29/29 trên 6 prefab — tên resolved khớp serialized key).
    • EffectManager: exportFormOverridenone (Mode B verbatim — file là side-effect module, dep-graph misclassify cc-component). → mọi scene game qua Gate 2: main (24) / login (26) / backpack (50). Full-434 prerequisite #0 DONE.
  • (Tương lai, KHÔNG block) dep-graph misclassify EffectManager (componentName=cc.Recycle, exportForm=cc-component) còn trong registry — đã workaround bằng exportFormOverride. Fix tận gốc (dep-graph phân biệt cc.X=cc.Class patch vs export) là cải thiện sạch hơn nhưng rủi ro cả 434 → để sau.
  • Aliased-node / event.target.<id> custom members (Gotcha 10 residual — only .node.<id> is covered by node-ext.d.ts).
  • already-ES modules (es-default 7 / es-named 1)DONE (Mode C): verbatim + .cv import rewrite. sdk.cv.ts (es-named, ES2022) converts EXIT=0. Residual is NOT conversion-related: dominant is narrow type-inference on a big verbatim object literal (var sdk = { …40 methods… } → dynamic sdk.X members, unfixable via d.ts — it’s a local literal, same class as the surgical this.X gap but on an object), plus a DOM-receiver category (HTMLDivElement.show/hide, window.X) the engine-ext generator could be extended to cover, plus verbatim quirks. Transitive plugin-.ts errors (Plugins/applovin/MAX.ts …) are PRE-EXISTING in those files, surfaced only because the import resolved.
  • mixins cc.Class option (0 in this project; throws if seen).
  • commonjs-named (exports.foo = ...) — none in this project; Mode B throws if seen.
  • Basename (Cocos-global) require coexistence rewrite (only relative .//../ handled).
  • .cv.ts.meta generation (new uuid) — cutover/t1k-cocos-base-uuid-verify concern.
  • t1k-cocos-base-dep-graph — produces the Export-Form Registry this codemod consumes (run it first).
  • plans/reports/2026-05-29-js-to-ts-migration-brainstorm.md — full migration design (§4 cutover, §5.2 skill stack, §7 target template).
  • .claude/handoffs/260530-dep-graph-hardening.md — session context.
  • t1k-cocos-cutover (not yet built) — strips .cv, transplants uuid, deletes .js.