TypeScript

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.

Mahdi Ben RhoumaMahdi Ben Rhouma
Updated
8 min read

You enable strict (or upgrade a project that just turned it on) and TypeScript stops the build:

text
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:

typescript
class Account {
  accountType = "user";   // OK — initialized by a property initializer
  retries = 0;
}

Fix 2 — Initialize in the constructor#

typescript
class GoodGreeter {
  name: string;
  constructor() {
    this.name = "hello";   // OK — assigned in the constructor body
  }
}
Why a helper method doesn't count

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."

typescript
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 !:

typescript
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.

Don't
profile!: UserProfile // slapped on just to make the red squiggle disappear
Do
profile?: UserProfile // honest: it may be undefined until loaded — now the compiler forces you to check

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:

typescript
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:

typescript
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:

typescript
@Input() id = 'default_id';     // default
@Input() movie?: Movie;         // optional
@Input({ required: true }) data!: Data;   // required + definite assignment

Last resort — disable the flag#

You can turn it off, but it removes the guarantee for every class in the project:

jsonc
// tsconfig.json
{ "compilerOptions": { "strictPropertyInitialization": false } }
Tradeoff

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.

When this won't work
  • You're on TypeScript before 2.7 — the flag and the prop!: T syntax don't exist there.
  • A commonly-repeated claim is that this check only applies when strictNullChecks is 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#

SituationFix
There's a sensible defaultInitialize at declaration
Always set during constructionAssign in the constructor body
Assigned by DI / framework / lifecycleDefinite assignment prop!: T
May genuinely be missingOptional prop?: T
Passed into the constructorParameter 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:

typescript
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

|

Have more questions? Contact us

Still stuck? Paste your error.

Paste the error message or stack trace — we check it against every fix and tool on the site.

Matching happens in your browser. When you search, a cleaned error signature is sent so we can spot errors we do not cover yet — never the raw text, your IP address or your e-mail. Do not paste passwords or keys. Details

Written by

Mahdi Ben Rhouma
Mahdi Ben Rhouma

Full-Stack Dev — Next.js & Supabase

Solo developer building SaaS products with Next.js and Supabase. Writing about production patterns the official docs skip.

Remote

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.