Skip to Content
Course content

86: Understanding Declaration Emit

Click on the "Edit" button in the top corner of the screen to edit your slide content.

If you've been writing TypeScript for a while, you're used to the compiler handling everything in one go: you write TS, it checks the types, and it spits out JS. But the moment you move from writing an application to writing a library that other people (or other teams) will consume, you hit a wall. I remember the first time I published a small internal utility for my team; I compiled my code to JavaScript, pushed it to our registry, and then watched my teammates spend an hour wondering why their IDEs were marking everything as any.

Shipping JavaScript and hoping for the best

The naive way to handle a library is to treat it like a plain JavaScript project. You run tsc, it generates your .js files, and you ship those. To the JavaScript runtime, this is perfect. The code runs exactly as expected. But to the TypeScript compiler in the consumer's project, your library is a black box. There are no type signatures, no interface definitions, and no hints about what the functions actually expect.

// In the library:
export function convertCurrency(amount: number, rate: number): number {
    return amount * rate;
}

// After naive compilation (only .js is shipped):
// export function convertCurrency(amount, rate) { return amount * rate; }

When your colleague imports convertCurrency, TypeScript has no way of knowing that amount should be a number. It defaults to any. At this point, you've essentially stripped away the primary reason for using TypeScript in the first place. You might be tempted to tell your team to just "cast it" or write their own .d.ts file to wrap your library, but that's a maintenance nightmare. You're asking your users to do your job for you.

Letting the compiler do the paperwork

The professional way to handle this is through Declaration Emit. Instead of just outputting the executable JavaScript, you tell TypeScript to also generate "Declaration Files" (.d.ts). These files contain only the type information—no actual logic—which acts as a map for any TypeScript project consuming your code.

You do this by adding "declaration": true to your tsconfig.json. When you run your build now, the compiler doesn't just give you index.js; it gives you index.d.ts. This file is the "contract" of your library. It tells the consumer: "I promise that convertCurrency takes two numbers and returns a number," without requiring them to compile your original source code.

// The generated index.d.ts (simplified)
export declare function convertCurrency(amount: number, rate: number): number;

Now, the experience is seamless. The consumer gets full autocomplete, type checking, and jump-to-definition capabilities, while the runtime only ever touches the lean JavaScript files. I've found that this is the single biggest difference between a "script" and a "package."

The cost of the extra files

Is there a catch? Not really, but there is a trade-off. Enabling declaration emit increases your build time slightly because the compiler has to perform a second pass to extract the type signatures. In massive projects with thousands of files, this can become noticeable. You might also find your dist folder looking a bit cluttered with twice as many files as before.

However, that's a small price to pay for type safety. If you're building a library, shipping .d.ts files isn't optional—it's the standard. If you don't provide them, you're essentially forcing your users to downgrade to JavaScript the moment they import your package. I'd rather wait an extra ten seconds for my build to finish than deal with a Slack message from a frustrated dev asking why their types are missing.




📋 Practical Task

Configuring a Typed String-Manipulation Library

You are building a utility library called text-toolkit. Your goal is to ensure that anyone who installs this library gets full type support without having to see your original TypeScript source code.

Requirements:

  • Create a file named index.ts with a function truncate(str: string, length: number): string and an exported interface ToolkitConfig.
  • Create a tsconfig.json file.
  • Configure the tsconfig.json so that the compiler generates both the JavaScript output AND the declaration files (.d.ts).
  • Ensure the output is directed to a folder named dist.

Verification: Run tsc in your terminal. Check your dist folder; it should contain both index.js and index.d.ts. If index.d.ts is missing, your configuration is incorrect.

Rating
0 0

There are no comments for now.

to be the first to leave a comment.