TS2564: Why Constructor Methods Don't Count as Init
TS2564 fires when a class field is typed but never guaranteed to be set. The fix depends on WHY it's unset: a default value, constructor assignment, the definite-assignment `!`, or an optional `?`. Picking the wrong one hides real bugs.
You enable strict (or upgrade a project that just turned it on) and TypeScript stops the build:
Property 'name' has no initializer and is not definitely assigned
in the constructor. (2564)This is TS2564, produced by the strictPropertyInitialization compiler flag. It is not noise — it is TypeScript refusing to let a class field be silently undefined. The right fix depends on why the field is unset, and choosing wrong trades a compile error for a runtime bug.
What triggers it#
strictPropertyInitialization (introduced in TypeScript 2.7, and part of the strict family) checks that "each instance property of a class gets initialized in the constructor body, or by a property initializer." If neither happens and the type is not optional, you get TS2564.
Because it ships inside strict, flipping "strict": true can surface TS2564 in code that compiled fine before — the 2.7 release notes warn about exactly this.
Fix 1 — Initialize at declaration#
If the field has a sensible default, set it inline:
class Account {
accountType = "user"; // OK — initialized by a property initializer
retries = 0;
}Fix 2 — Initialize in the constructor#
class GoodGreeter {
name: string;
constructor() {
this.name = "hello"; // OK — assigned in the constructor body
}
}This is the #1 confusion with TS2564. From the TypeScript Classes handbook: the field must be initialized in the constructor itself. TypeScript does not analyze methods you invoke from the constructor, "because a derived class might override those methods and fail to initialize the members."
class Bad {
name: string; // TS2564 — still flagged
constructor() { this.init(); }
init() { this.name = "x"; } // not analyzed
}Fix 3 — Definite assignment assertion !#
When something outside the constructor reliably assigns the property — a DI container, a framework, a lifecycle hook — tell the compiler with !:
class Service {
private repo!: Repository; // "!" = definite assignment assertion
constructor() { this.wire(); }
wire() { this.repo = makeRepo(); }
}The TypeScript 2.7 notes describe ! as relaying "that a variable is indeed assigned for all intents and purposes, even if TypeScript's analyses cannot detect so" — and cite dependency injection as the canonical reason it exists.
The risk: ! is unchecked. If the property is never actually assigned, you get a silent runtime undefined and zero compile-time warning. Use it only when assignment is genuinely guaranteed.
Fix 4 — Mark it optional or union with undefined#
If the property may legitimately be absent, say so — and let the compiler force you to handle the undefined case:
class Form {
email?: string; // optional
address: string | undefined; // explicit union, equivalent intent
}The handbook's guidance: "if we truly meant for [it] to potentially be undefined, we should have declared it" that way.
Fix 5 — Constructor parameter properties#
The public/private/protected/readonly prefix on a constructor parameter declares and assigns the field in one step, so it satisfies the check with no body:
class Point {
constructor(public readonly x: number, private y: number) {}
// x and y are declared and assigned — no TS2564
}Framework note: Angular @Input#
Angular treats @Input properties as optional by design. The idiomatic options are a default value, ?, or — for @Input({ required: true }) — the definite assignment assertion:
@Input() id = 'default_id'; // default
@Input() movie?: Movie; // optional
@Input({ required: true }) data!: Data; // required + definite assignmentLast resort — disable the flag#
You can turn it off, but it removes the guarantee for every class in the project:
// tsconfig.json
{ "compilerOptions": { "strictPropertyInitialization": false } }Disabling it reintroduces the "uninitialized field is silently undefined" bug class everywhere — strictly worse than a targeted ! or ? on the one property that needs it.
- You're on TypeScript before 2.7 — the flag and the
prop!: Tsyntax don't exist there. - A commonly-repeated claim is that this check only applies when
strictNullChecksis also on. The official pages I cite don't state that dependency — don't rely on it without verifying against your own compiler version.
Which fix to use#
| Situation | Fix |
|---|---|
| There's a sensible default | Initialize at declaration |
| Always set during construction | Assign in the constructor body |
| Assigned by DI / framework / lifecycle | Definite assignment prop!: T |
| May genuinely be missing | Optional prop?: T |
| Passed into the constructor | Parameter property |
Official references: strictPropertyInitialization, TypeScript 2.7 release notes, Classes handbook.
Fix 6 — the declare modifier, for fields a base class already assigns#
There's a sixth situation the five fixes above don't cover cleanly: a subclass that
declares a typed field purely to describe a value its base class (or a decorator on
the base class) already assigns in its own constructor. The definite assignment
assertion (!) from Fix 3 half-solves this — it silences TS2564 — but it doesn't solve
a second, separate problem that shows up alongside it once class field emit is involved.
Modern TypeScript, by default, emits a class field declared with an explicit type and no
initializer as a real assignment in the generated JavaScript, following the current
class-fields specification. When useDefineForClassFields is in effect (it is
by default once your target is ES2022 or later), that emitted assignment uses
Object.defineProperty semantics. A subclass that simply redeclares a field to give it a
type — without ever assigning it directly — still gets that emitted assignment, and it
runs after the base class constructor finishes. If the base class (or a decorator on
it) already set that property to a real value, the subclass's own field declaration can
silently overwrite it back to undefined the moment the subclass constructor completes,
even with prop!: T telling the type checker to trust it.
TypeScript added the declare modifier for exactly this case:
class Base {
constructor() {
// assigns `id` at runtime, outside any subclass's knowledge
Object.assign(this, { id: 'generated-by-base' });
}
}
class Derived extends Base {
declare id: string; // ✅ describes the field, emits NOTHING
}declare tells the compiler "this field exists and has this type, but do not generate
any code for it at all" — no Object.defineProperty call, no assignment, nothing to
overwrite what the base class already did. Because nothing is emitted, strict PropertyInitialization does not apply to it either: there's no runtime field for the
compiler to worry about leaving uninitialized, so TS2564 does not fire on a declare
field in the first place, which is why it belongs alongside the ! and ? fixes above
rather than replacing either of them — pick declare specifically when a base class or
decorator, not your own constructor, is the one doing the assigning.
The failure mode this fixes — a value that's correct immediately after the base class
constructor runs and then reverts to undefined once the subclass constructor finishes
— is easy to misdiagnose as an async timing bug, because it only appears with
inheritance and only with certain target/useDefineForClassFields combinations. If
you're chasing a field that "resets itself" across a class hierarchy after bumping
target in tsconfig.json, the compiler target change itself is the more likely
trigger; the broader set of target-driven breakages in a major TypeScript upgrade is
covered in the TypeScript major-version migration guide for Next.js/Supabase.
Further reading: Fix Cannot find name 'console' in TypeScript (TS2584), Cannot use JSX unless the '--jsx' flag is provided.
Frequently Asked Questions
Still stuck? Paste your error.
Paste the error message or stack trace — we check it against every fix and tool on the site.
One email a month — the latest explained errors
What the site published that month — mostly Next.js, Supabase and Node errors, explained. You confirm by email; unsubscribe any time in one click.
Continue Reading
Fix An index signature parameter type cannot be a union
TypeScript blocks union types in index signatures. Learn why the error fires and how to fix it with mapped types or the Record utility type in under a minute.
Fix Cannot find name 'console' in TypeScript (TS2584)
console.log fails to type-check in a Node project. See which two tsconfig settings hide console, why the compiler hint points the wrong way, and the fix.
Fix Cannot find type definition file for 'body-parser'
Why one broken folder under node_modules/@types fails the whole project, why restarting VS Code sometimes clears it, and what TypeScript 6.0 changed.


