Documentation Index
Fetch the complete documentation index at: https://resources.devweekends.com/llms.txt
Use this file to discover all available pages before exploring further.
Modules & Configuration
Understanding how TypeScript handles modules and project configuration is essential for building real-world applications. Many developers skip this chapter and then spend hours debugging mysterious “cannot find module” errors or wondering why their build output is wrong. Modules are how you organize code into separate files, andtsconfig.json is how you tell the compiler what rules to enforce and how to produce output. Getting these right from the start saves days of pain later.
1. ES Modules
TypeScript fully supports ES modules (ESM), the standard JavaScript module system. ES modules useimport/export syntax and are the direction the entire JavaScript ecosystem is moving. If you have used require() in Node.js, ESM is the modern replacement with better static analysis (TypeScript can check imports at compile time), tree-shaking support (bundlers can remove unused exports), and standardized behavior across browsers and Node.js.
Named Exports
Default Exports
Mixed Exports
Import Aliases
Namespace Import
Type-Only Imports
import type is not just good practice — it prevents accidental side effects. If a module has top-level code that runs on import, import type guarantees that code will not execute. Some bundlers also rely on import type for better tree-shaking.
2. Re-exports (Barrel Files)
Create a single entry point for multiple modules. Barrel files (typically namedindex.ts) act as a “public API” for a directory — consumers import from the barrel rather than reaching into individual files. This is one of the most common organizational patterns in TypeScript projects.
Selective Re-exports
3. Module Resolution
How TypeScript finds imported modules. Module resolution is the process of turning an import path ('./utils', 'express') into an actual file on disk. Getting this wrong produces the dreaded “Cannot find module” errors. Understanding these strategies eliminates an entire category of configuration headaches.
Relative Imports
Non-relative Imports
Module Resolution Strategies
4. Declaration Files (.d.ts)
Type definitions for JavaScript libraries. Declaration files (.d.ts) are the bridge between TypeScript and the vast JavaScript ecosystem. They describe the types of a JavaScript library without modifying its source code — like a separate “instruction manual” that tells TypeScript what shapes a library’s exports have.
Using @types Packages
Creating Declaration Files
Ambient Declarations
5. Namespaces
TypeScript’s original module system (still useful for type organization).Nested Namespaces
6. tsconfig.json
The TypeScript configuration file. Runtsc --init to generate one. This is the single most important file in a TypeScript project — it controls what gets compiled, how strict the type checking is, what JavaScript version to target, and how modules are resolved. A misconfigured tsconfig.json leads to false confidence (too lenient) or developer frustration (too strict without understanding why).
Essential Options
Strict Mode Options
Additional Checks
Path Aliases
7. Project References
Split large projects into smaller, independently compiled pieces.Main tsconfig.json
Package tsconfig.json
Build Command
8. Common Configurations
Node.js Backend
React Frontend
Library
9. Module Augmentation
Extend existing modules and types.Extending Third-Party Types
Extending Global Types
10. Build Tools Integration
package.json Scripts
ESLint Configuration
Vitest Configuration
Summary
| Concept | Example |
|---|---|
| Named Export | export { User } |
| Default Export | export default class User |
| Named Import | import { User } from './User' |
| Default Import | import User from './User' |
| Type-only Import | import type { User } from './User' |
| Barrel File | export * from './module' |
| Declaration File | .d.ts files |
| Namespace | namespace App { } |
| Path Aliases | "@models/*": ["src/models/*"] |
| Project References | "references": [{ "path": "./pkg" }] |
| Module Augmentation | declare module 'express' { } |
What’s Next?
Congratulations! You’ve completed the TypeScript Crash Course. You now have a solid foundation in:- ✅ Type annotations and inference
- ✅ Functions and generics
- ✅ Interfaces and type aliases
- ✅ Classes and OOP patterns
- ✅ Advanced types and utility types
- ✅ Modules and configuration
Continue Learning
React + TypeScript
Build type-safe React applications with hooks, context, and components.
Node.js + TypeScript
Create robust backend APIs with Express, NestJS, or Fastify.
Full-Stack TypeScript
End-to-end type safety with tRPC, Prisma, and Next.js.
Advanced Patterns
Explore design patterns, dependency injection, and architecture.
Interview Deep-Dive
Q: Explain the difference between 'type-only imports' and regular imports. When do they matter, and what problem do they solve?
Q: Explain the difference between 'type-only imports' and regular imports. When do they matter, and what problem do they solve?
Strong Answer:Type-only imports (
import type { User } from './types') are a TypeScript feature that tells the compiler “this import is only used for type checking — remove it entirely from the output JavaScript.”- Why they exist: TypeScript’s type erasure removes type annotations from the output, but the
importstatement itself is JavaScript — it stays in the output and executes at runtime. If you import an interface (import { User } from './types') andUseris only used as a type annotation, the compiled JavaScript still hasimport { User } from './types', which imports a module that might have side effects, adds to your bundle size, or causes circular dependency issues — all for a value that is never used at runtime. - What
import typedoes: It guarantees the import is erased completely. The compiled JavaScript has no trace of it. No module is loaded, no code executes, no bundle size impact. This is critical for tree-shaking: bundlers like Webpack and Rollup can more aggressively remove dead code when they know an import is type-only. - When they matter most: Circular dependencies. If module A imports a type from module B, and module B imports a value from module A, a regular import creates a circular dependency that can cause runtime errors (one module loads before the other is ready). With
import type, module A’s import is erased, breaking the cycle. I have seen production applications crash on startup because of circular dependencies that only existed for type imports. - The
isolatedModulesflag: When enabled (required by tools like esbuild and Babel that compile files individually), TypeScript requires that imports used only as types are marked withimport type. Without this flag, TypeScript can figure it out during full-project compilation, but single-file compilers cannot.
verbatimModuleSyntax is the successor to isolatedModules and preserveValueImports. When enabled, TypeScript enforces a simple rule: any import that is not explicitly import type will be emitted in the JavaScript output as-is. There is no “smart” detection of whether an import is type-only. This makes the behavior predictable across all build tools: what you write is what you get. It does not replace import type — it makes import type mandatory for type-only imports, which is arguably the correct default since it eliminates ambiguity and makes the developer’s intent explicit.Q: What are barrel files (index.ts re-exports), and why do some teams ban them? What is the performance trade-off?
Q: What are barrel files (index.ts re-exports), and why do some teams ban them? What is the performance trade-off?
Strong Answer:Barrel files are index.ts files that re-export from multiple modules, creating a single import point:
import { User, Product, Order } from './models' instead of three separate imports.- The appeal: Clean import paths, centralized API surface, easy refactoring (move a file and only update the barrel). They feel elegant and reduce import clutter.
- Why some teams ban them: Performance. When you import one thing from a barrel file, the module bundler (or Node.js) must load and evaluate every module the barrel re-exports.
import { User } from './models'triggers loadingUser.ts,Product.ts,Order.ts, and every other module in the barrel — even though you only needUser. In a large codebase with hundreds of models, this can add seconds to startup time and megabytes to bundle size. - Tree-shaking is not a complete fix: Bundlers like Webpack and Rollup can theoretically tree-shake unused re-exports, but this only works for statically analyzable, side-effect-free modules. If any module in the barrel has top-level side effects (database connection, global event listener, polyfill), the bundler must include it. In practice, barrel files defeat tree-shaking more often than developers realize.
- The IDE performance impact: Large barrel files slow down TypeScript’s language service. When you type
import { } from './models', the IDE must resolve every export from every re-exported module to populate autocomplete. In monorepos with deep barrel chains (barrel re-exports from another barrel), this cascades and can make autocomplete take 2-3 seconds. - My recommendation: Use barrel files for small, cohesive modules (a UI component library with 10-15 components). Avoid them for large, heterogeneous collections (all models, all services, all utilities). Use direct imports for performance-critical paths.
@models/User mapping to src/models/User) are often used alongside barrel files to create clean import paths. The common misconfiguration is setting up aliases in tsconfig.json but forgetting to configure the same aliases in the bundler (Webpack’s resolve.alias, Vite’s resolve.alias, or the tsconfig-paths package for Node.js). TypeScript compiles without error because it resolves the alias during type checking, but the runtime or bundler cannot find the module because it uses a different resolution algorithm. The result: “Module not found” errors that only appear at build time or runtime, not during type checking.Q: Walk me through tsconfig.json's 'strict' flag. What does it actually enable, and should every project use it?
Q: Walk me through tsconfig.json's 'strict' flag. What does it actually enable, and should every project use it?
Strong Answer:
"strict": true is an umbrella flag that enables 8 individual strict checks. Understanding each one matters because teams sometimes need to enable them incrementally during migration.noImplicitAny: Variables and parameters without type annotations default toanyin non-strict mode. With this flag, TypeScript errors if it cannot infer a type, forcing you to annotate. This is the single most impactful strict flag becauseanydefeats the entire purpose of TypeScript.strictNullChecks: Without this,nullandundefinedare assignable to every type.const name: string = nullcompiles. With this flag,nullmust be explicitly included in the type:string | null. This catches null reference errors at compile time — the most common runtime error in JavaScript.strictFunctionTypes: Enables contravariant checking for function parameter types. Without it, TypeScript allows unsound assignments where a function accepting a base type is assigned to a variable expecting a function accepting a derived type. This is technically a type safety hole that strict mode closes.strictBindCallApply: Checks thatFunction.prototype.bind,.call, and.applyare called with correct argument types. Without it, these methods acceptanyarguments.strictPropertyInitialization: Class properties must be initialized in the constructor or have a definite assignment assertion (!). Prevents accessing uninitialized properties.noImplicitThis: Errors whenthishas an implicitanytype (typically in standalone functions). Forces explicitthisparameter annotations.useUnknownInCatchVariables: Makescatch(e)variablesunknowninstead ofany, forcing you to narrow the error type before accessing properties.alwaysStrict: Emits'use strict'in every output file.
noImplicitAny (catches the most bugs), then strictNullChecks (the hardest to retrofit but most valuable), then the rest.Follow-up: What is the ‘noUncheckedIndexedAccess’ flag, and why is it not included in ‘strict’?It adds | undefined to every index signature and array index access. const first = arr[0] becomes T | undefined. It is not in strict because the TypeScript team decided it was too disruptive for existing codebases — it requires null checks on every array access, which is verbose in code that already validates array bounds. I recommend enabling it for new projects that handle external data heavily (API responses, user input), and leaving it off for projects where arrays are always pre-validated.Q: How do Project References work in TypeScript, and when would you use them in a monorepo?
Q: How do Project References work in TypeScript, and when would you use them in a monorepo?
Strong Answer:Project References split a large TypeScript project into smaller, independently compiled units. They solve the problem of “compiling 500,000 lines of TypeScript takes 45 seconds even though I only changed one file.”
- How they work: Each sub-project has its own
tsconfig.jsonwith"composite": true. A roottsconfig.jsonlists all sub-projects in"references": [{"path": "./packages/core"}, {"path": "./packages/api"}]. When you runtsc --build, TypeScript compiles each sub-project independently and caches the output (.d.tsfiles and.tsbuildinfo). On subsequent builds, it only recompiles sub-projects whose source files changed. - The
compositeflag requirement:"composite": truetells TypeScript “this sub-project must produce declaration files and a build info cache.” It enforces that the sub-project is self-contained: all source files must be underrootDir, and the project must emit.d.tsfiles so other sub-projects can import types without re-analyzing the source. - When to use them: Monorepos with multiple packages that depend on each other. Example:
packages/core(shared types and utilities),packages/api(backend, depends on core),packages/web(frontend, depends on core). Without project references,tscanalyzes all three packages every time. With project references, changing a file inpackages/webonly recompilespackages/webbecausepackages/coreandpackages/apiare cached. - The performance impact: In a monorepo with 10 packages and 200K lines of TypeScript, project references can reduce incremental build times from 30+ seconds to 2-3 seconds. The initial build is similar, but subsequent builds are dramatically faster.
- The trade-off: More configuration complexity. Each package needs its own
tsconfig.json. Dependencies between packages must be declared explicitly inreferences. Circular dependencies between packages are a compile error (which is actually a good constraint).
tsc --build uses Project References to skip unchanged sub-compilations. The practical setup is: Turborepo orchestrates tsc --build for each package, caching the entire output. Project References handle the TypeScript-specific incremental compilation within a single tsc invocation.