Right, so you want to run TypeScript on the edge. Not the browser’s edge, but the network’s edge—closer to your users than a traditional server. It’s a fantastic idea: lower latency, faster responses. But let’s be clear, this isn’t your grandpa’s Node.js environment. These are lean, mean, stripped-down JavaScript runtimes, and your TypeScript has to play by their rules. I’m talking about AWS Lambda@Edge and Vercel Edge Functions. They’re similar in goal but different in execution, and understanding those differences is the difference between a smooth launch and pulling your hair out at 2 AM.

First, the big, beautiful lie you need to get over: you’re not actually running TypeScript. Shocker, I know. These environments run JavaScript. Your TypeScript code gets transpiled to JavaScript before it’s deployed. This is a crucial mental model to adopt. Your deployment pipeline (be it Vercel’s magic, the AWS CDK, or some shell script you hacked together) is responsible for this compilation step. If your types are wrong, the compiler will yell at you on your machine, not in production. That’s actually a good thing.

The Deployment Pipeline is Your New Best Friend

Forget running tsc manually. You’ll be integrating the build step into your deployment process. Vercel does this automatically if you point it at a directory with a tsconfig.json. It’s delightfully boring, which is the highest compliment I can give a platform.

AWS, as is its tradition, requires a bit more elbow grease. You’re likely using the AWS CDK or Serverless Framework. Your job is to tell those tools to run tsc (or better yet, esbuild because it’s blindingly fast) and then deploy the resulting .js files from your dist output directory.

Here’s a realistic example of a serverless.yml that handles this for a Lambda@Edge function:

# serverless.yml

myEdgeFunction:
  handler: dist/handler.handler # Points to the COMPILED JS file
  runtime: nodejs18.x
  events:
    - cloudFront:
        eventType: viewer-request
        origin: s3://my-bucket

package:
  patterns:
    - '!./**' # Exclude everything by default
    - 'dist/**' # Then, explicitly include the dist directory
    - 'node_modules/**'

custom:
  esbuild:
    bundle: true
    minify: true
    sourcemap: true
    target: node18
    packager: npm
    watch:
      pattern: ['src/**/*.ts'] # Watch your TypeScript source
      ignore: ['dist']

The key takeaway: your configuration deploys the dist folder, not the src folder. The TypeScript source is for you and your compiler; the cloud only gets the JavaScript.

The Cold, Hard Truth of Runtime Context

These aren’t full Node.js environments. They’re missing modules you might take for granted. fs.readFileSync? Probably not. Trying to connect to a database with a traditional TCP connection? Think again. The whole point is that these functions are stateless and short-lived.

This is where your TypeScript skills really shine. Use the type system to enforce these constraints. Make your own life harder at compile time to make your deployment life easier.

// This function signature is a lie in an Edge environment.
// It implies you can do async I/O, which you might not be able to finish.
const badHandler = async (event: Event): Promise<Response> => {
  const data = await fetchSomeDataFromYourDatabase(); // 😬 Danger!
  return { statusCode: 200, body: JSON.stringify(data) };
};

// A better approach: pre-compute what you can and make dependencies explicit.
interface Env {
  API_KEY: string; // Environment variables are your friends here
}

// The handler knows it only gets these specific things.
const betterHandler = async (event: Event, env: Env): Promise<Response> => {
  // Use global fetch (available in these runtimes) for external calls.
  // But be warned! Your function has a strict timeout.
  const response = await fetch('https://api.you-control.com/data', {
    headers: { 'Authorization': env.API_KEY }
  });

  if (!response.ok) {
    // Handle errors gracefully. No console.log here—it goes to the void.
    // Use the runtime's logging system (e.g., `console` logs to CloudWatch in AWS)
    return { statusCode: 502, body: 'Upstream service failed' };
  }

  const data = await response.json();
  return { statusCode: 200, body: JSON.stringify(data) };
};

Taming the tsconfig.json Beast

Your tsconfig.json needs to be configured for the specific runtime. For most Edge environments, including Lambda@Edge and Vercel, you’re targeting a modern JavaScript environment.

{
  "compilerOptions": {
    "target": "ES2022",
    "module": "ESNext",
    "moduleResolution": "node",
    "esModuleInterop": true,
    "strict": true,
    "skipLibCheck": true,    // A necessary evil for some type definitions
    "forceConsistentCasingInFileNames": true,
    "outDir": "dist",
    "rootDir": "src"
  },
  "include": ["src/**/*"],
  "exclude": ["node_modules", "dist"]
}

Why ES2022? Because that’s what the Node.js 18.x runtime (a common target for these) supports. Using an older target like ES2017 would mean missing out on modern features these runtimes actually have, forcing the compiler to output unnecessary and slower polyfill code. Don’t fight the runtime; embrace it.

The biggest pitfall? Assuming your local Node.js environment and the edge runtime are identical. They’re not. Test your built artifacts. Seriously. Run node dist/handler.js against a local test event to simulate the cold start. It will save you countless deployment cycles. This is where the witty, brilliant friend (me) stops joking and gets direct: test your builds, or suffer the consequences.