Skip to content

RFC: "@cannotBeUndefined" for optional properties #163

Open
@octogonz

Description

@octogonz

Suppose an interface has optional members (possibly introduced by Partial<T>). For example:

class Person {
    public name: string = '';
    public age: number | undefined = undefined;

    public constructor(traits: IPersonTraits = {}) {
        this.update(traits);
    }

    public update(traits: IPersonTraits) {
        if (traits.name) {
            this.name = traits.name;
        }
        if (traits.hasOwnProperty("age")) {
            this.age = traits.age;
        }
    }
}

interface IPersonTraits {
    /**
     * The person's name or `""` if not known.
     */
    name?: string;

    /**
     * The person's age, or `undefined` if not known.
     */
    age?: number | undefined;
}

TypeScript currently allows undefined to be specified for both IPersonTraits.name and IPersonTraits.age. In other words, name?: string is equivalent to name?: string | undefined, and in fact gets normalized to that in the emitted .d.ts file. This is a gap in the type system. (See microsoft/TypeScript#13195 which proposes to improve the language to distinguish between a missing member versus an undefined value.)

This gap can lead to bugs. For example:

let person = new Person();

// CORRECT USAGE:
person.update({
    name: 'hello',
    age: undefined  // <-- overwrites "age" with undefined, as intended
});

// MISTAKE:
person.update({
    name: undefined,  // <-- assigns undefined to "name", which is not an allowed value
    age: 13
});

The compiler cannot catch this mistake, and in fact the shipping .d.ts files will contain the normalized name?: string | undefined declaration, which misleadingly implies that undefined is a meaningful value for name.

A possible solution

Suppose we introduce a TSDoc modifier tag call @cannotBeUndefined, which would be used like this:

interface IPersonTraits {
    /**
     * The person's name or `""` if not known.
     * @cannotBeUndefined
     */
    name?: string;

    /**
     * The person's age, or `undefined` if not known.
     */
    age?: number | undefined;
}

Benefits:

  • The tag will be appear in the normalized .d.ts file
  • The tag will be displayed in the VS Code tooltip, providing clear documentation
  • We could implement a TSLint rule that looks for this tag, and if found, reports an error for an expression that is possibly undefined

What do you think? Does this seem useful enough to be a standard tag?

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions