Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Strict property initialization checks in classes #20075

Merged
merged 19 commits into from
Nov 20, 2017

Conversation

ahejlsberg
Copy link
Member

@ahejlsberg ahejlsberg commented Nov 16, 2017

This PR implements a new --strictPropertyInitialization compiler option to guard against uninitialized properties in class instances. Strict property initialization checking verifies that each instance property declared in a class either (a) has a type that includes undefined or (b) has an explicit initializer or an assignment to the property in the constructor. The --strictPropertyInitialization option has no effect unless --strictNullChecks is also enabled.

The --strictPropertyInitialization switch is part of the --strict family of switches, meaning that it defaults to on in --strict mode. This PR is therefore a breaking change only in --strict mode.

In this example

// Compile with --strict
class Test {
    a: number;  // Error, property not initialized
    b?: string;  // Ok, type includes undefined
}

an error is reported on property a because its type doesn't include undefined and the property declaration doesn't specify an initializer or assign a value to the property in the constructor. The error can be fixed by changing the type of a to include undefined, by specifying an initial value for a, or by assigning a value to a in the constructor. For example, this is ok:

// Compile with --strict
class Test {
    a: number = 0;  // Ok
    b?: string;
}

A strict property initialization check is satisfied by assignments in the constructor only when all possible code paths include assignments to the particular property.

// Compile with --strict
class Test {
    a: number;
    constructor(startAtZero: boolean) {
        if (startAtZero) {
            this.a = 0;
        }
        else {
            this.a = 1;
        }
    }
}

In the example above, an error would occur if either of the if statement branches omitted the assignment to this.a.

Strict property initialization checks guard against observing uninitialized properties in the constructor body. For example:

// Compile with --strict
class Test {
    a: number;
    constructor() {
        let x = this.a;  // Error
        this.a = 0;
    }
}

Strict property initialization checks also guard against observing uninitialized properties after the constructor returns. However, it is possible to observe uninitialized properties in methods that are called from the constructor (or from a base constructor). For example:

// Compile with --strict
class Test {
    a: number;
    constructor() {
        this.foo();
        this.a = 0;
    }
    foo() {
        // Uninitialized value of 'a' observable here
    }
}

Strict property initialization checks only apply to properties that are declared with proper identifiers. It does not apply to properties with computed names or properties with names specified as string or numeric literals (this is analogous to how control flow analysis isn't applied to properties accessed using obj["foo"] syntax).

// Compile with --strict
class Test {
    a: number;  // Error, not initialized
    "hello world": number;  // No check
}

In cases where the properties of a class instance are initialized by external means (such as dependency injection) or where it is known that an initialization method is always called before other methods, strict property initialization checks can be suppressed by including definite assignment assertions (#20166) in the declarations of the properties.

// Compile with --strict
class C {
    a!: number;  // Use ! to suppress error
    b!: string;  // Use ! to suppress error
    initialize() {
        this.a = 0;
        this.b = "hello";
    }
}

Alternatively, // @ts-ignore comments can be used to suppress the errors, or the code can simply be compiled without the --strictPropertyInitialization option.

Fixes #8476.

@RyanCavanaugh
Copy link
Member

Add check for abstract properties

@itsMapleLeaf
Copy link

Note that strict property initialization checks only guard against observing uninitialized properties after the constructor returns. It is still possible to observe uninitialized properties in methods that are called from the constructor (or from a base constructor).

Is there a reason TS can't check if the called functions access the uninitialized value, then report the error in the constructor?

@weswigham
Copy link
Member

@kingdaro we only do control flow analysis across function bounds for immediately invoked function expressions; to do otherwise would be error-prone in the presence of aliasing, so we conservatively avoid doing so.

@ahejlsberg ahejlsberg merged commit 148dc4e into master Nov 20, 2017
@abramobagnara
Copy link

abramobagnara commented Nov 21, 2017

What about initializing helpers?
Suppose that constructor calls a method that complete the initialization (e.g. the developer has put this initialization in a separate method because it should be called also after object construction).
What you propose to handle this case?

@ahejlsberg
Copy link
Member Author

@abramobagnara I'd propose one of:

  • Don't use the --strictPropertyInitialization option.
  • Rewrite the code to include initializers or assignments in the constructor.
  • Use definite assignment assertions introduced by Definite assignment assertions #20166.

@ahejlsberg
Copy link
Member Author

I have edited the PR description to reflect #20166.

@mihailik
Copy link
Contributor

The --strictPropertyInitialization option has no effect unless --strictNullChecks is also enabled.

Will be really useful to have a warning in such case.

@mihailik
Copy link
Contributor

mihailik commented Nov 21, 2017

Also, does this PR handle cases where error is thrown before all fields initialised?

class Car {
  color: string;

  constructor() {
    if (!carsSupportedByBrowser) {
     console.log('Cars not supported');
     throw new Error('Not supported');
    }

    this.color = 'red';
  }
}

@mihailik
Copy link
Contributor

And a quirkier variation of previous case:

class Car {
  color: string;

  constructor() {
    if (!carsSupportedByBrowser) {
     console.log('Cars not supported ', this); <--- see we expose instance with uninitialised color
     throw new Error('Not supported');
    }

    this.color = 'red';
  }
}

@ahejlsberg
Copy link
Member Author

Also, does this PR handle cases where error is thrown before all fields initialised?

It is fine to throw an exception before all properties are initialized, and yes you can expose an object that isn't fully initialized to the outside (just like you can call methods before the object is fully initialized).

@mhegazy mhegazy mentioned this pull request Nov 22, 2017
8 tasks
@microsoft microsoft locked and limited conversation to collaborators Jun 14, 2018
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Check non-undefined properties are initialized in the constructor with --strictNullChecks
7 participants