Skip to content
GuidesDocs 1.3.x

Erasable Syntax ​

Node.js 25.2.0 and later can execute .ts files directly by stripping type annotations at load time. This is not full TypeScript compilation -- it only removes type syntax, leaving the remaining JavaScript intact. Your TypeScript code must be valid JavaScript after type annotations are removed. The framework repository's contributor environment separately requires Node.js 26 or later.

How Native TypeScript Works ​

bash
# Run TypeScript directly -- no tsc needed
node src/index.ts

The key implication: your TypeScript code must be valid JavaScript after type annotations are removed. This is called erasable syntax only.

The erasableSyntaxOnly Constraint ​

Enable this in your tsconfig.json:

json
{
  "compilerOptions": {
    "erasableSyntaxOnly": true
  }
}

This makes TypeScript's type checker enforce that you only use syntax that can be erased, catching violations at development time.

What You Cannot Use ​

The following TypeScript features generate runtime code and cannot be erased:

No enum ​

typescript
// WRONG: enum generates runtime code
enum Status {
  PENDING = 1,
  ACTIVE = 2,
  CLOSED = 3,
}

// CORRECT: use const object with 'as const'
const Status = {
  PENDING: 1,
  ACTIVE: 2,
  CLOSED: 3,
} as const;

type Status = typeof Status[keyof typeof Status];
// Status = 1 | 2 | 3

This pattern gives you:

  • Type safety (same as enum)
  • Runtime values (accessible in code)
  • String literal type via keyof typeof
  • No extra compilation step

No namespace with Runtime Code ​

typescript
// WRONG: namespace with runtime value
namespace MyApp {
  export const version = '1.0.0';
}

// CORRECT: use a module
export const version = '1.0.0';

TIP

Type-only namespaces (containing only types/interfaces) are allowed since they are fully erasable:

typescript
// OK: type-only namespace
namespace MyTypes {
  export interface Config {
    port: number;
  }
}

No Parameter Properties ​

typescript
// WRONG: parameter properties generate assignment code
class Server {
  constructor(private port: number) {}
}

// CORRECT: explicit property declaration
class Server {
  private port: number;
  constructor(port: number) {
    this.port = port;
  }
}

No Legacy Decorators ​

Decorators are not converted by type stripping. Do not use decorator syntax on the native execution path unless your selected runtime supports it directly; otherwise use an application build step:

typescript
// WRONG: legacy decorator
@Injectable()
class UserService {}

// Check the selected runtime before using standard decorators as JavaScript.

Import Rules ​

Explicit import type ​

With verbatimModuleSyntax: true, you must separate type imports from value imports:

typescript
// CORRECT: explicit type import
import type { ConnectRouter } from '@connectrpc/connect';
import type { SayHelloRequest } from '#gen/greeter_pb.ts';

// CORRECT: value import
import { create } from '@bufbuild/protobuf';
import { GreeterService } from '#gen/greeter_pb.ts';

// CORRECT: mixed import with inline type
import { GreeterService, type SayHelloRequest } from '#gen/greeter_pb.ts';

// WRONG: type imported as value (caught by verbatimModuleSyntax)
import { SayHelloRequest } from '#gen/greeter_pb.ts';
//       ^ This is a type, must use 'import type'

File Extensions in Imports ​

Use .ts extensions in relative imports of source files. During compilation, rewriteRelativeImportExtensions rewrites these extensions in emitted JavaScript; native execution does not rewrite paths and requires the actual source extension:

typescript
// CORRECT: .ts extension for source files
import { greeterService } from './services/greeterService.ts';
import type { Config } from './config.ts';

// CORRECT: no extension for package imports
import { createServer } from '@connectum/core';
import { create } from '@bufbuild/protobuf';

// WRONG: .js extension for source files (outdated convention)
import { greeterService } from './services/greeterService.js';

// WRONG: no extension for relative imports
import { greeterService } from './services/greeterService';

Generated Code (#gen/) Imports ​

The extension used in generated protobuf imports (#gen/*) follows the import_extension option in your buf.gen.yaml. Run-directly projects like this one (and the example projects) generate .ts and import with .ts, which requires allowImportingTsExtensions: true in tsconfig.json. Compiled packages that ship JS instead set import_extension=.js and import with .js.

For a run-directly project (import_extension=.ts):

typescript
// CORRECT: .ts for generated protobuf files (import_extension=.ts)
import { GreeterService } from '#gen/greeter_pb.ts';
import type { SayHelloRequest } from '#gen/greeter_pb.ts';

// WRONG: extension that does not match buf.gen.yaml import_extension
import { GreeterService } from '#gen/greeter_pb.js';

The #gen/ path alias is defined in package.json via the imports field ("#gen/*": "./gen/*").

Node.js Built-in Modules ​

Always use the node: prefix for Node.js built-in modules:

typescript
// CORRECT
import { readFileSync } from 'node:fs';
import { resolve } from 'node:path';
import { setTimeout } from 'node:timers/promises';

// WRONG: no node: prefix
import { readFileSync } from 'fs';

tsconfig.json Configuration ​

Here is the recommended tsconfig.json for Connectum projects:

json
{
  "compilerOptions": {
    // No compilation -- TypeScript runs natively
    "noEmit": true,

    // ECMAScript and module targets
    "target": "esnext",
    "module": "nodenext",
    "moduleResolution": "nodenext",

    // Native TypeScript execution constraints
    "erasableSyntaxOnly": true,
    "verbatimModuleSyntax": true,
    "rewriteRelativeImportExtensions": true,

    // Strict type checking
    "strict": true,
    "esModuleInterop": true,
    "skipLibCheck": true,
    "forceConsistentCasingInFileNames": true
  },
  "include": ["src/**/*.ts", "gen/**/*.ts"],
  "exclude": ["node_modules"]
}

Key Options Explained ​

OptionValuePurpose
noEmittrueNo compilation output -- TypeScript is for type checking only
erasableSyntaxOnlytrueEnforce erasable-only syntax
verbatimModuleSyntaxtrueRequire explicit import type
rewriteRelativeImportExtensionstrueAllow .ts extensions in imports
modulenodenextNode.js ESM module system
moduleResolutionnodenextNode.js module resolution algorithm