Headshot of Marius Schulz
Marius Schulz Front End Engineer

TypeScript 2.7: Strict Property Initialization

TypeScript 2.7 introduced a new compiler option for strict property initialization checks in classes. If the --strictPropertyInitialization flag is enabled, the type checker verifies that each instance property declared in a class either

  • has a type that includes undefined,
  • has an explicit initializer, or
  • is definitely assigned to in the constructor.

The --strictPropertyInitialization option is part of the family of compiler options that is enabled automatically when the --strict flag is set. As with all the other strict compiler options, you can set --strict to true and selectively opt out of strict property initialization checks by setting --strictPropertyInitialization to false.

Note that the --strictNullChecks flag must be set (either directly or indirectly via --strict) in order for --strictPropertyInitialization to have any effect.

Alright, let's see strict property initialization checks in action. Without the --strictPropertyInitialization flag enabled, the following code type-checks just fine, but produces a TypeError at runtime:

class User {
  username: string;
}

const user = new User();

// TypeError: Cannot read property 'toLowerCase' of undefined
const username = user.username.toLowerCase();

The reason for the runtime error is that the username property holds the value undefined because there's no assignment to that property. Therefore, the call to the toLowerCase() method fails.

If we enable --strictPropertyInitialization, the type checker raises an error:

class User {
  // Type error: Property 'username' has no initializer
  // and is not definitely assigned in the constructor
  username: string;
}

Let's look at four different ways we can properly type our User class to make the type error go away.

Solution #1: Allowing undefined

One way to make the type error go away is to give the username property a type that includes undefined:

class User {
  username: string | undefined;
}

const user = new User();

Now, it's perfectly valid for the username property to hold the value undefined. Whenever we want to use the username property as a string, though, we first have to make sure that it actually holds a string and not the value undefined, e.g. using typeof:

// OK
const username = typeof user.username === "string"
  ? user.username.toLowerCase()
  : "n/a";

Solution #2: Explicit Property Initializer

Another way to make the type error go away is to add an explicit initializer to the username property. This way, the property holds a string value right away and is not observably undefined:

class User {
  username = "n/a";
}

const user = new User();

// OK
const username = user.username.toLowerCase();

Solution #3: Assignment in the Constructor

Perhaps the most useful solution is to add a username parameter to the constructor, which is then assigned to the username property. This way, whenever an instance of the User class is constructed, the caller has to provide the username as an argument:

class User {
  username: string;

  constructor(username: string) {
    this.username = username;
  }
}

const user = new User("mariusschulz");

// OK
const username = user.username.toLowerCase();

We could simplify the User class by removing the explicit assignment to the class field and adding the public modifier to the username constructor parameter. The following is still type-correct:

class User {
  constructor(public username: string) {}
}

const user = new User("mariusschulz");

// OK
const username = user.username.toLowerCase();

Note that strict property initialization requires each property to be definitely assigned in all possible code paths in the constructor. The following code is therefore not type-correct because in some cases, we leave the username property uninitialized:

class User {
  // Type error: Property 'username' has no initializer
  // and is not definitely assigned in the constructor.
  username: string;

  constructor(username: string) {
    if (Math.random() < 0.5) {
      this.username = username;
    }
  }
}

Solution #4: Definite Assignment Assertion

If a class property neither has an explicit initializer nor a type including undefined, the type checker requires that property to be initialized directly within the constructor; otherwise, strict property initialization checks will fail. This is problematic if you want to initialize a property within a helper method or have a dependency injection framework initialize it for you. In these cases, you have to add a definite assignment assertion (!) to that property's declaration:

class User {
  username!: string;

  constructor(username: string) {
    this.initialize(username);
  }

  private initialize(username: string) {
    this.username = username;
  }
}

const user = new User("mariusschulz");

// OK
const username = user.username.toLowerCase();

By adding a definite assignment assertion to the username property, we're telling the type checker that it can expect the username property to be initialized, even if it cannot detect that on its own. It is now our responsibility to make sure the property is definitely assigned to after the constructor returns, so we have to careful; otherwise, the username property can be observably undefined and we're back to the TypeError at runtime.

This post is part of the TypeScript Evolution series:

  1. TypeScript 2.0: Non-Nullable Types
  2. TypeScript 2.0: Control Flow Based Type Analysis
  3. TypeScript 2.0: Acquiring Type Declaration Files
  4. TypeScript 2.0: Read-Only Properties
  5. TypeScript 2.0: Tagged Union Types
  6. TypeScript 2.0: More Literal Types
  7. TypeScript 2.0: The never Type
  8. TypeScript 2.0: Built-In Type Declarations
  9. TypeScript 2.1: async/await for ES3/ES5
  10. TypeScript 2.1: External Helpers Library
  11. TypeScript 2.1: Object Rest and Spread
  12. TypeScript 2.1: keyof and Lookup Types
  13. TypeScript 2.1: Mapped Types
  14. TypeScript 2.1: Improved Inference for Literal Types
  15. TypeScript 2.1: Literal Type Widening
  16. TypeScript 2.1: Untyped Imports
  17. TypeScript 2.2: The object Type
  18. TypeScript 2.2: Dotted Properties and String Index Signatures
  19. TypeScript 2.2: Null-Checking for Expression Operands
  20. TypeScript 2.2: Mixin Classes
  21. TypeScript 2.3: Generic Parameter Defaults
  22. TypeScript 2.3: The --strict Compiler Option
  23. TypeScript 2.3: Type-Checking JavaScript Files with --checkJs
  24. TypeScript 2.3: Downlevel Iteration for ES3/ES5
  25. TypeScript 2.4: String Enums
  26. TypeScript 2.4: Weak Type Detection
  27. TypeScript 2.4: Spelling Correction
  28. TypeScript 2.4: Dynamic import() Expressions
  29. TypeScript 2.5: Optional catch Binding
  30. TypeScript 2.6: JSX Fragment Syntax
  31. TypeScript 2.7: Numeric Separators
  32. TypeScript 2.7: Strict Property Initialization
  33. TypeScript 2.8: Per-File JSX Factories
  34. TypeScript 2.8: Conditional Types
  35. TypeScript 2.8: Mapped Type Modifiers