2

Difference from similar question

In error TS2339: Property 'x' does not exist on type 'Y', the subject was the indexable type. Here is not, so most likely the other solutions required.

Problem

interface INumberPropertySpecification__CommonParameters {
  readonly type: PROPERTIES_TYPES.NUMBER; // enum
  readonly numberType: NUMBER_TYPES; // enum
}

export type NumberPropertySpecification =
      INumberPropertySpecification__Required |
      INumberPropertySpecification__HasDefault |
      INumberPropertySpecification__Optional;

In above code, type NumberPropertySpecification is the union for below cases:

  1. The property is required. In this case, library user MUST comprehend it, so I make him to explicitly specify required: true
interface INumberPropertySpecification__Required extends INumberPropertySpecification__CommonParameters {
    readonly required: true;
    readonly invalidValueSubstitution?: number;
}

// Example:

const requiredNumberPropertySpecification: INumberPropertySpecification__Required = {
  type: PROPERTIES_TYPES.NUMBER,
  numberType: NUMBER_TYPES.ANY_REAL_NUMBER,
  required: true // user MUST comprehend it, so I make him to explicitly specify it
}
  1. The property has default value; it means that it is not required. I don't want to make user to specify required: false because it's obviously.
interface INumberPropertySpecification__HasDefault extends INumberPropertySpecification__CommonParameters {
    readonly defaultValue: number;
}

// Example
const requiredNumberPropertySpecification: INumberPropertySpecification__HasDefault = {
  type: PROPERTIES_TYPES.NUMBER,
  numberType: NUMBER_TYPES.ANY_REAL_NUMBER,
  default: 1
}
  1. The property is optional. In this case, user MUST comprehend it, so I make him to explicitly specify required: false:
interface INumberPropertySpecification__Optional extends INumberPropertySpecification__CommonParameters {
    readonly required: false;
}

// Example:

const requiredNumberPropertySpecification: INumberPropertySpecification__Optional = {
  type: PROPERTIES_TYPES.NUMBER,
  numberType: NUMBER_TYPES.ANY_REAL_NUMBER,
  required: false // user MUST comprehend it, so I make him to explicitly specify it
}

Errors

We can not check targetPropertySpecification.required === true. In this case, TypeScript type check algorithm makes overkill, because when targetPropertySpecification.required is undefined, no JavaScript error will occur (even if just if(targetPropertySpecification.required), but who uses "@typescript-eslint/strict-boolean-expressions": can not write such as).

enter image description here

Same song with defaultValue (the isUndefined):

enter image description here

Takeshi Tokugawa YD
  • 670
  • 5
  • 40
  • 124

1 Answers1

1

Your code is very similar to discriminated union, but you're using required filed for this.

JavaScript (and TypeScript) distinguish between existing property which has undefined value and non-existent property. So to make your code works, you should add optional properties required and defaultValue to all interfaces.

interface INumberPropertySpecification__Required extends INumberPropertySpecification__CommonParameters {
  readonly required: true;
  readonly invalidValueSubstitution?: number;
  readonly defaultValue?: undefined;
}

Type of defaultValue is explicitly undefined and it is set to be optional.

The similar modifications should be done for other two interfaces.

interface INumberPropertySpecification__HasDefault extends INumberPropertySpecification__CommonParameters {
  readonly defaultValue: number;
  readonly required?: undefined;
}

interface INumberPropertySpecification__Optional extends INumberPropertySpecification__CommonParameters {
  readonly required: false;
  readonly defaultValue?: undefined;
}

Now you can still do

const targetPropertySepcification: NumberPropertySpecification = {
  required: true,
  type: PROPERTIES_TYPES.NUMBER,
  numberType: NUMBER_TYPES.T1
}

You're not required to provide value for defaultValue. It is set to undefined.

And following code will work

function f1(targetPropertySepcification: NumberPropertySpecification) {
  if (targetPropertySepcification.required === true) {
    // targetPropertySepcification is of type INumberPropertySpecification__Required
    const n: number | undefined = targetPropertySepcification.invalidValueSubstitution;
  } else if (targetPropertySepcification.required === false) {
    // targetPropertySepcification is of type INumberPropertySpecification__Optional
  } else if (targetPropertySepcification.required === undefined) {
    // targetPropertySepcification is of type INumberPropertySpecification__HasDefault
    const n: number = targetPropertySepcification.defaultValue;
  }

  if (targetPropertySepcification.defaultValue === undefined) {
    // Here targetPropertySepcification is of type INumberPropertySpecification__Required | INumberPropertySpecification__Optional 
    //  and required property is of type true | false, or simply boolean
    const rt: boolean = targetPropertySepcification.required
  }
}
Fyodor Yemelyanenko
  • 11,264
  • 1
  • 30
  • 38