skip to content

Strictness & Correctness Flags

You will learn what the strict umbrella actually turns on, which safety flags it deliberately leaves off, and the single-file compilation contract that modern toolchains require. Interviewers use these flags to find out whether you understand exactly where TypeScript stops protecting you.

part ofTypeScriptoverview, primer and where to startread it →
on this pageshow

explore

questions

15

In TypeScript, what does `import type { User } from './models'` mean, and how does it differ from a plain `import { User } from './models'`?

level: juniorimportance: must knowfreq 68%

answer

  1. about what survives compilation
  2. a promise, not an optimisation
  3. type positions only, never a value
  4. guarantees erasure; plain imports rely on elision
  5. inline form marks single specifiers

basics

~20 s

import type declares the binding as types only: TypeScript always erases that statement from the emitted JavaScript, and the name cannot be used as a value. A plain import is erased only when the compiler decides nothing in the file uses it at runtime.

solid answer

~50 s

`import type` is an explicit promise to the compiler that everything the statement brings in is used only in type positions. Two things follow. First, the statement is **guaranteed** to be erased — nothing is emitted for it, so it cannot even trigger the imported module's side effects. Second, the binding is usable only where a type is expected: writing `new User()` or passing `User` as a value is a compile error. A plain `import { User }` is different in kind: TypeScript's *import elision* will drop it from the output if it sees the name used only as a type, but that decision depends on whole-program information, and if you later use `User` as a value the import quietly reappears in the emitted JavaScript. There is also an inline form, `import { type User, createUser } from './models'`, which marks individual specifiers when one statement carries both types and values.

go deeper

for a junior

Know that import type brings in only a type, never a runtime value, and that the whole statement vanishes from the compiled JavaScript. Be able to say what breaks if you then try to call or construct that binding.

for a middle

Explain import elision as the implicit alternative, and why an explicit type keyword is decidable from a single file while elision needs whole-program information. Show the inline type specifier form and the matching export type syntax.

for a senior

Show the runtime failure mode: a type-only import left unmarked survives a per-file transpiler and produces a module that requests an export nobody emitted. Be able to talk about relying on side-effect imports separately from type imports.

for a principal

Own the convention. Decide whether the codebase writes type-only imports by hand and has the compiler enforce it, weighing a one-time mechanical migration against the class of build-tool-only runtime failures it retires for every future contributor.

## The problem this syntax solves TypeScript's central rule is that types are erased: the compiler emits JavaScript with the type layer removed. That creates an awkward case for imports. `import { User } from './models'` is ordinary JavaScript syntax that, at runtime, actually loads `./models` and binds one of its exports. But if `User` is an interface, there is no runtime export named `User` at all — the interface emitted nothing. Keeping that import in the output would produce a module that fails to link. TypeScript's historical answer is **import elision**: while emitting, the compiler looks at how each imported name is used and drops the ones that are only ever used in type positions, dropping the whole statement when nothing is left. Elision is convenient but implicit, and its correctness depends on information from other files. ## What the explicit form does ```ts import type { User } from "./models"; ``` This declares up front that the statement is type-only. Two guarantees follow. **It is always erased.** The emitted JavaScript contains no trace of the statement, so the module is never loaded because of it. If `./models` had a top-level side effect you were relying on, a type-only import will not run it — you would need a separate `import "./models";`. **The binding is a type, not a value.** Everything below is legal, because each use is a type position: ```ts let current: User; function save(u: User): Promise<void> { /* ... */ } interface Session { user: User } ``` And this is a compile error, because it uses the name where a runtime value is required: ```ts const u = new User(); // error: 'User' cannot be used as a value console.log(User); // error ``` That second guarantee is why the form is useful even for classes. A class produces *both* a type and a runtime value; importing it with `import type` deliberately takes only the type half, and the compiler will stop you the moment someone reaches for the value half. ## The inline modifier When one module gives you both types and values, you do not need two statements: ```ts import { type User, createUser } from "./models"; ``` `createUser` is a real value import and survives into the output; `type User` is dropped. There is a matching export form: ```ts export type { User }; export { type User, createUser } from "./models"; ``` ## Why interviewers care Elision requires the compiler to know what every imported name *is*, which requires reading the other file. Tools that compile one file at a time — esbuild, swc, Babel's TypeScript transform — never read the other file. They see `import { User } from './models'` and have no way to tell an interface from a function, so they must keep the import. If `User` was type-only, the emitted module now imports something that does not exist. Explicit `import type` removes the guesswork: the `type` keyword is *syntax*, visible in the single file being transpiled, so any tool can delete it correctly without cross-file knowledge. That is why compiler options exist to require it, and why modern codebases write it by hand rather than relying on elision. ## Practical guidance - Reach for `import type` (or the inline `type` modifier) whenever an import is genuinely type-only. It costs nothing and documents intent. - Do not use it for a module you import *for* its side effect; use a bare `import "./setup";` for that. - Remember the direction of the guarantee: `import type` guarantees erasure, whereas a plain import only *might* be erased. When in doubt, being explicit is always the safe side. - The same reasoning applies to exports: re-exporting a type through a plain `export { ... } from` is the mirror-image hazard, and `export type { ... } from` is the mirror-image fix.

  • If a module needs to be loaded for its side effects and you only use a type from it, what do you write?
    Two statements: `import type { Handler } from "./plugin";` for the type, plus a bare `import "./plugin";` to keep the side effect. A type-only import emits nothing at all, so on its own it will never load the module and the side effect silently disappears.
  • A class is both a type and a value. What breaks if you import it with `import type`?
    Only the value half. Annotations, `implements` clauses and `extends` in an interface still work, but `new Klass()`, `Klass.staticMethod()`, `instanceof Klass` and passing `Klass` as an argument all become compile errors, because the binding was declared type-only and is erased from the output.
  • Why does the inline form `import { type A, b }` exist when `import type { A }` already does?
    So a single module's types and values can travel in one statement instead of two. The statement is kept in the emitted JavaScript for `b`, with the `type A` specifier removed. It also lets a tool that reads only this file decide correctly, since the marker is right there in the syntax.

saying these in an interview costs you the question

  • Calls import type a style preference with no compile-time effect
  • Says import type makes the import faster or smaller at runtime
  • Expects a type-only import to still run the module's side effects
  • Uses an import type binding in a new expression or instanceof check
  • Assumes a plain import of an interface always disappears from the output

context

open as a page

With TypeScript's noImplicitAny flag enabled, why does `function greet(name) {}` fail to compile while the `u` in `users.map(u => u.id)` needs no annotation at all?

level: juniorimportance: must knowfreq 70%

basics

~20 s

noImplicitAny only errors where the compiler has no type to infer. A standalone parameter has no source of type information, so it would silently become any; a callback parameter is contextually typed from the signature it is passed to, so its type is inferred rather than implicit.

open as a page

What does TypeScript's `isolatedModules` compiler flag do, and does enabling it change the JavaScript that `tsc` emits?

level: middleimportance: must knowfreq 57%

basics

~20 s

isolatedModules changes no output at all. It is a checking-only flag: it makes the compiler report source constructs that cannot be compiled correctly by a tool that sees one file at a time, so the code stays portable to per-file transpilers.

open as a page

In a TypeScript project's tsconfig.json, what does setting "strict": true actually do, and how would you keep it on while turning off just one of the checks it implies?

level: middleimportance: must knowfreq 78%

basics

~20 s

The strict option in tsconfig.json is not one check: it is a shorthand that switches on a whole family of individual flags, including noImplicitAny, strictNullChecks, strictFunctionTypes, strictBindCallApply, strictPropertyInitialization, noImplicitThis, useUnknownInCatchVariables and alwaysStrict. Setting any member explicitly overrides the umbrella.

open as a page

In a TypeScript tsconfig.json, does setting "strict": true turn on every correctness check the compiler offers? Name checks it leaves off and explain why they are separate options.

level: juniorimportance: should knowfreq 48%

basics

~10 s

No. strict switches on one fixed family of type-soundness options; a separate set of correctness checks stays off until you list each one by name, including noUncheckedIndexedAccess, exactOptionalPropertyTypes, noImplicitOverride, noImplicitReturns, noFallthroughCasesInSwitch and noUnusedLocals.

open as a page

In TypeScript, what does the exactOptionalPropertyTypes compiler option change about a property declared retries?: number, and how do you keep allowing an explicit undefined?

level: middleimportance: should knowfreq 30%

basics

~20 s

exactOptionalPropertyTypes separates absent from present-and-undefined. With it on, retries?: number means the property may be missing but must hold a real number when present, so assigning { retries: undefined } is an error; declare retries?: number | undefined to allow both.

open as a page

In TypeScript, what does the noImplicitOverride compiler option require, which class members does it apply to, and what bug is the override keyword designed to catch?

level: middleimportance: should knowfreq 28%

basics

~20 s

noImplicitOverride requires the override keyword on any member that redefines a concrete member inherited from a base class, including properties and accessors. It catches renamed or deleted base members, whose overrides would otherwise silently become brand-new members.

open as a page

In TypeScript, what does the noUncheckedIndexedAccess compiler option change about the type of rows[0] for const rows: string[], and which accesses does it deliberately leave alone?

level: middleimportance: should knowfreq 52%

basics

~10 s

With noUncheckedIndexedAccess on, every read through an array index or an index signature gains undefined: rows[0] is string | undefined rather than string. Declared properties, fixed tuple positions and for...of iteration are unaffected.

open as a page

Why does TypeScript's `isolatedModules` flag reject references to an ambient `declare const enum`, and what does a per-file transpiler do with an ordinary `const enum`?

level: middleimportance: should knowfreq 30%

basics

~20 s

A const enum is normally compiled away by inlining its member values, which requires reading the declaring file. An ambient const enum emits no runtime object at all, so a tool that reads one file has nothing to inline and nothing to reference — hence the error.

open as a page

What does TypeScript's `verbatimModuleSyntax` flag change about how imports and exports are emitted, and which older options does it replace?

level: middleimportance: should knowfreq 44%

basics

~10 s

verbatimModuleSyntax replaces import elision with one syntactic rule: anything written with a type modifier is dropped entirely, and everything else is emitted exactly as written. Added in TypeScript 5.0, it supersedes importsNotUsedAsValues and preserveValueImports.

open as a page

In a TypeScript project compiled with strict enabled, what is the static type of `e` in `try { ... } catch (e) { ... }`, and what do you have to do before reading `e.message`?

level: middleimportance: should knowfreq 45%

basics

~20 s

Under strict, the useUnknownInCatchVariables flag types a catch clause variable as unknown rather than any, so reading any property off it is a compile error. You must narrow first — typically with an instanceof Error check — before touching e.message.

open as a page

A TypeScript class compiled with strict reports "Property 'name' has no initializer and is not definitely assigned in the constructor". Which compiler flag produces that error, and what are the legitimate ways to satisfy it?

level: middleimportance: should knowfreq 42%

basics

~20 s

The strictPropertyInitialization flag, enabled by strict and requiring strictNullChecks, produces that error. Satisfy it by giving the field an initializer, assigning it unconditionally in the constructor, or admitting it may be missing by making the type optional or including undefined.

open as a page

Your team wants to adopt TypeScript's beyond-strict correctness flags — noUncheckedIndexedAccess, exactOptionalPropertyTypes and noUnusedLocals — on a large existing codebase. How would you sequence the rollout, and which flag do you expect to cost the most?

level: seniorimportance: should knowfreq 38%

basics

~20 s

Measure each flag's error count first by enabling it alone on the command line, then adopt one flag per change. Expect noUncheckedIndexedAccess to dominate, since it touches every array index and dictionary lookup, and watch that its fixes are real checks rather than assertions.

open as a page

A TypeScript package's index.ts contains `export { Config } from './config';` where Config is an interface. Built with `tsc` the package works, but built by a per-file transpiler such as esbuild or swc, consumers crash with an error saying './config' provides no export named 'Config'. Why does the same source behave differently, and how do you fix it?

level: seniorimportance: should knowfreq 38%

basics

~20 s

tsc reads ./config, sees Config is an interface, and drops it from the re-export. A per-file transpiler never reads that file, so it emits a real re-export of a binding that the emitted config.js does not have. Fix it with export type, and enable isolatedModules so the compiler catches every such site.

open as a page

You set "strict": true in the tsconfig.json of an established TypeScript codebase and tsc reports several thousand errors. How do you get the project onto strict without a big-bang rewrite?

level: seniorimportance: should knowfreq 50%

basics

~20 s

Split the work by flag and by file. Enable the cheap members of the strict family first, then adopt strictNullChecks and noImplicitAny through a second tsconfig that extends the base, sets strict, and lists a growing set of already-clean files that CI checks alongside the loose build.

open as a page