Type-Safe Package Exports with TypeScript
Modern JavaScript and TypeScript projects often leverage package exports to control which parts of a package are publicly available. This allows for better organization, encapsulation, and dependency management. This challenge focuses on effectively defining and utilizing these export types in TypeScript to ensure robustness and prevent unintended access to internal modules.
Problem Description
Your task is to create a TypeScript package structure that demonstrates the use of different export types. Specifically, you need to define internal modules that are not meant to be imported directly by consumers of your package, and public modules that are. You will then implement type definitions that accurately reflect this export strategy, ensuring that TypeScript can correctly infer and validate imports.
What needs to be achieved:
- Organize modules: Structure your package into internal and public facing modules.
- Define exports: Use
package.json's"exports"field to control what is exposed externally. - Create type definitions: Generate
.d.tsfiles that accurately represent the public API of your package. - Demonstrate type safety: Show how TypeScript prevents imports from internal modules and allows imports from public modules.
Key requirements:
- The package should have at least one public export.
- The package should have at least one internal module that is not exported publicly.
- TypeScript's type checking should prevent importing from the internal module when using the package as a dependency.
- Type definitions should be clear and accurate for the public exports.
Expected behavior:
When another TypeScript project imports your package, it should only be able to import from the explicitly exported public entry points. Attempting to import from internal modules should result in a TypeScript compilation error.
Edge cases to consider:
- What happens if you try to import a sub-path that is not explicitly defined in
package.json's"exports"? (TypeScript should ideally handle this gracefully, andpackage.jsonconfiguration is key here).
Examples
Example 1:
Assume a package structure like this:
my-package/
├── src/
│ ├── internal/
│ │ └── utils.ts
│ └── public/
│ └── api.ts
├── index.ts
├── package.json
└── tsconfig.json
And src/internal/utils.ts contains:
export function internalHelper(): string {
return "This is an internal helper.";
}
And src/public/api.ts contains:
import { internalHelper } from '../internal/utils';
export function publicGreeting(name: string): string {
// We can use internal helpers within the package itself
const message = internalHelper();
return `Hello, ${name}! From public API. ${message}`;
}
And index.ts is the main entry point:
export * from './src/public/api';
And package.json has the following "exports" field:
{
"name": "my-package",
"version": "1.0.0",
"main": "dist/index.js",
"types": "dist/index.d.ts",
"exports": {
".": {
"import": "./dist/index.mjs",
"require": "./dist/index.js",
"types": "./dist/index.d.ts"
}
},
// ... other fields
}
Consumer project (consumer-app/):
// This should WORK
import { publicGreeting } from 'my-package';
console.log(publicGreeting('World'));
Consumer project (consumer-app/ - Error Case):
// This should FAIL with a TypeScript error
import { internalHelper } from 'my-package/src/internal/utils';
Explanation:
The package.json "exports" configuration dictates that only the root export (.) is publicly accessible. The internal module src/internal/utils is not mapped in "exports", so attempting to import it directly from the consumer project will result in a TypeScript error because it's considered an internal implementation detail and not part of the public API.
Constraints
- Your solution must be implemented in TypeScript.
- The project structure should be clear and logical.
- The generated
.d.tsfiles must be accurate. - You should demonstrate the type safety by showing the expected compilation errors for invalid imports.
- The solution should be runnable and testable.
Notes
- Consider using a build tool like
tscor a bundler (like Webpack, Rollup, or esbuild) to compile your TypeScript to JavaScript and generate.d.tsfiles. - Pay close attention to the
"exports"field inpackage.json. Modern Node.js and bundlers heavily rely on this for package resolution and type checking. - Think about how different
exportsconditions (e.g.,import,require,types) impact how your package is consumed. - To effectively demonstrate the error, you'll need to simulate consuming your package from another TypeScript project (e.g., by using
npm linkor a monorepo setup).