← Back to DevBytes

SWC TypeScript: Strongly Typed Applications

SWC TypeScript: Strongly Typed Applications

SWC (Speedy Web Compiler) is a super-fast TypeScript and JavaScript compiler written in Rust. When combined with TypeScript's type system, SWC enables developers to build strongly typed applications that compile at blazing speeds — often 20 to 70 times faster than the default TypeScript compiler (tsc). This tutorial walks through everything you need to know to build robust, strongly typed applications using SWC.

What Is SWC?

SWC is an extensible Rust-based platform for the next generation of fast developer tools. It handles compilation, minification, bundling, and transformation of JavaScript and TypeScript code. Unlike Babel or tsc, SWC leverages Rust's performance characteristics to deliver near-instant feedback during development.

It is important to understand that SWC's primary role is transpilation — stripping types and converting modern syntax into compatible JavaScript. SWC does not perform full type checking by default. For type checking, you still use tsc in a separate step, typically in CI or via an editor integration.

Why SWC Matters for TypeScript Applications

Setting Up a SWC TypeScript Project

Start by initializing a new project and installing the required dependencies.

mkdir swc-ts-app && cd swc-ts-app
npm init -y
npm install --save-dev @swc/cli @swc/core typescript @types/node

Create a basic project structure:

swc-ts-app/
├── src/
│   └── index.ts
├── .swcrc
├── tsconfig.json
└── package.json

Configuring TypeScript

Even though SWC handles transpilation, you still need a tsconfig.json for type checking and editor support. Create one with strict settings:

{
  "compilerOptions": {
    "target": "ES2020",
    "module": "ESNext",
    "moduleResolution": "Bundler",
    "strict": true,
    "noImplicitAny": true,
    "strictNullChecks": true,
    "noUnusedLocals": true,
    "noUnusedParameters": true,
    "noImplicitReturns": true,
    "esModuleInterop": true,
    "skipLibCheck": true,
    "forceConsistentCasingInFileNames": true,
    "outDir": "./dist",
    "rootDir": "./src",
    "declaration": true,
    "isolatedModules": true
  },
  "include": ["src/**/*"],
  "exclude": ["node_modules", "dist"]
}

The isolatedModules flag is critical when using SWC. Because SWC transpiles each file independently, certain TypeScript features (like const enum and namespace re-exports) require this setting to ensure compatibility.

Configuring SWC

Create a .swcrc file to control how SWC transpiles your TypeScript:

{
  "$schema": "https://swc.rs/schema.json",
  "env": {
    "targets": {
      "node": "16"
    }
  },
  "jsc": {
    "parser": {
      "syntax": "typescript",
      "tsx": false,
      "decorators": true,
      "dynamicImport": true
    },
    "transform": {
      "legacyDecorator": true
    },
    "target": "es2020",
    "paths": {
      "@/*": ["./src/*"]
    }
  },
  "module": {
    "type": "es6"
  },
  "minify": false
}

Key configuration options explained:

Writing Strongly Typed Code

Now let's write some actual TypeScript code that leverages the type system. Create src/index.ts:

// src/types.ts
export interface User {
  id: string;
  name: string;
  email: string;
  role: UserRole;
  createdAt: Date;
}

export type UserRole = "admin" | "editor" | "viewer";

export interface Repository<T> {
  findById(id: string): Promise<T | null>;
  save(entity: T): Promise<T>;
  delete(id: string): Promise<boolean>;
}
// src/userRepository.ts
import { Repository, User, UserRole } from "./types";

export class UserRepository implements Repository<User> {
  private users: Map<string, User> = new Map();

  async findById(id: string): Promise<User | null> {
    return this.users.get(id) ?? null;
  }

  async save(entity: User): Promise<User> {
    this.users.set(entity.id, entity);
    return entity;
  }

  async delete(id: string): Promise<boolean> {
    return this.users.delete(id);
  }

  async findByRole(role: UserRole): Promise<User[]> {
    return Array.from(this.users.values()).filter(
      (user) => user.role === role
    );
  }
}
// src/index.ts
import { UserRepository } from "./userRepository";
import { User } from "./types";

async function main(): Promise<void> {
  const repository = new UserRepository();

  const adminUser: User = {
    id: "1",
    name: "Alice",
    email: "alice@example.com",
    role: "admin",
    createdAt: new Date(),
  };

  await repository.save(adminUser);

  const found = await repository.findById("1");
  if (found) {
    console.log(`Found user: ${found.name} (${found.role})`);
  }

  const admins = await repository.findByRole("admin");
  console.log(`Total admins: ${admins.length}`);
}

main().catch((error) => {
  console.error("Application error:", error);
  process.exit(1);
});

Building and Running the Application

Add scripts to your package.json to handle both transpilation and type checking:

{
  "name": "swc-ts-app",
  "version": "1.0.0",
  "scripts": {
    "build": "swc src -d dist",
    "type-check": "tsc --noEmit",
    "start": "node dist/index.js",
    "dev": "swc src -d dist --watch && node dist/index.js"
  }
}

Run the build and type check separately:

npm run build
npm run type-check
npm start

The build command uses SWC for fast transpilation, while type-check uses tsc in --noEmit mode to validate types without producing output files. This separation is the recommended workflow for SWC-based TypeScript projects.

Using SWC with Node.js Directly

For development, you can run TypeScript files directly with SWC without precompiling. Install the SWC register hook:

npm install --save-dev @swc-node/register

Then run your TypeScript files directly:

node --import @swc-node/register/esm-register src/index.ts

This approach is excellent for rapid development and testing, while still benefiting from TypeScript's type system in your editor.

Integrating SWC with a Bundler

For larger applications, you will likely use a bundler. Here is an example using SWC with esbuild or webpack via the swc-loader:

npm install --save-dev swc-loader webpack webpack-cli

Create a webpack.config.js:

const path = require("path");

module.exports = {
  entry: "./src/index.ts",
  output: {
    filename: "bundle.js",
    path: path.resolve(__dirname, "dist"),
  },
  module: {
    rules: [
      {
        test: /\.ts$/,
        exclude: /node_modules/,
        use: {
          loader: "swc-loader",
          options: {
            jsc: {
              parser: {
                syntax: "typescript",
              },
              target: "es2020",
            },
          },
        },
      },
    ],
  },
  resolve: {
    extensions: [".ts", ".js"],
  },
};

Best Practices for SWC TypeScript Applications

Configuring Jest with SWC

Testing is a critical part of strongly typed applications. Configure Jest to use SWC for transpiling TypeScript test files:

npm install --save-dev jest @swc/jest

Add to your package.json or jest.config.js:

module.exports = {
  testEnvironment: "node",
  transform: {
    "^.+\\.ts$": "@swc/jest",
  },
  testMatch: ["**/__tests__/**/*.test.ts"],
  moduleFileExtensions: ["ts", "js", "json"],
};

Write a test for the repository:

// src/__tests__/userRepository.test.ts
import { UserRepository } from "../userRepository";
import { User } from "../types";

describe("UserRepository", () => {
  let repository: UserRepository;

  beforeEach(() => {
    repository = new UserRepository();
  });

  const mockUser: User = {
    id: "test-1",
    name: "Test User",
    email: "test@example.com",
    role: "editor",
    createdAt: new Date("2024-01-01"),
  };

  it("should save and retrieve a user", async () => {
    await repository.save(mockUser);
    const found = await repository.findById("test-1");
    expect(found).toEqual(mockUser);
  });

  it("should return null for non-existent user", async () => {
    const found = await repository.findById("non-existent");
    expect(found).toBeNull();
  });

  it("should filter users by role", async () => {
    await repository.save(mockUser);
    await repository.save({ ...mockUser, id: "test-2", role: "viewer" });
    const editors = await repository.findByRole("editor");
    expect(editors).toHaveLength(1);
    expect(editors[0].id).toBe("test-1");
  });
});

Run the tests:

npx jest

Handling Path Aliases

Path aliases improve code readability. Configure them in both tsconfig.json and .swcrc. For runtime resolution, use a tool like tsc-alias or configure your bundler:

npm install --save-dev tsc-alias

Update your build script:

"scripts": {
  "build": "swc src -d dist && tsc-alias"
}

Conclusion

SWC combined with TypeScript offers the best of both worlds: the safety and developer ergonomics of a strong static type system, paired with compilation speeds that keep your feedback loop tight. By separating concerns — using SWC for fast transpilation and tsc for thorough type checking — you can build scalable, maintainable applications without sacrificing performance. Follow the best practices outlined in this tutorial, keep your configurations aligned, and leverage SWC across your build, development, and testing workflows to get the most out of strongly typed TypeScript applications.

— Ad —

Google AdSense will appear here after approval

← Back to all articles