A custom error to be thrown when numeric values are out of bounds Why did I make this? I'm building an interactive data structure visualization, and when certain errors are thrown from certain parts of the code, they are displayed on screen for the user. I have not included that displaying logic.

The if checks seem somewhat smelly to me, but they might be simple enough to the point that they're fine. I'm interested in best practices, ANY criticism is welcome.

export class OutOfBoundsError extends AppError { 
// AppError code is not included, but it is just an extremely simple
//abstract class I needed so I could instanceof to check whether a caught
// error was thrown by my own code
// If you want, pretend I'm extending JS's RangeError instead

constructor(valueName: 'length' | 'index' | 'position', value: number,
              bounds?: {
                lowerBoundInclusive?: number;
                upperBoundInclusive?: number
              }) {
    super();
    const lower = bounds?.lowerBoundInclusive;
    const upper = bounds?.upperBoundInclusive;
    if (lower === undefined) {
      if (upper === undefined) {
        this.message = `${valueName} of ${value} is out of bounds`;
      } else {
        this.message = `${valueName} must be less than or equal to ${upper} but\
         ${value} was given`;
      }
    } else {
      if (upper === undefined) {
        this.message = `${valueName} must be greater than or equal to ${lower}\
         but ${value} was given`;
      } else {
        this.message = `${valueName} must be between ${lower} and ${upper} but\
         ${value} was given`;
      }
    }
  }
}
```
This seems good.
I noticed a couple of things, I will try to explain down below.


Lack of type definition
For the sake of readability, it would be great to have some type definitions to make the code look easier to read.


type ValueName = 'length' | 'index' | 'position';

interface Bounds {
    lowerBoundInclusive?: number;
    upperBoundInclusive?: number;
}

export class OutOfBoundsError extends AppError {
    // ...

    constructor(name: ValueName, value: number, bounds?: Bounds) {
        // ...
    }
}


Since Bounds is an structured type, it deserves to be an interface.
The point of defining types is not just the code itself, helps in case of documentation, code hints (code completion, parameters information, ...) and reusability.
In case of reusability, types should be separated from implementations.




message internal property misplace
Inside your constructor, you are setting the value of message. Unlike Javascript, Typescript needs placeholders for internal properties.
Maybe you had, indeed. Your code does not compile, otherwise.
Anyways, internal properties must be declared between the class definition and its constructor.


export class OutOfBoundsError extends AppError {
    private message: string;

    constructor(name: ValueName, value: number, bounds?: Bounds) {
        // ...
    }
}





Truthy and falsy assertions
Avoid checking with explicit undefined, use if (value) instead.
If value is a number type and in fact you are expecting a 0 to be a valid value, Javascript will coerce it into a false. In that case, check your value like so


if (value != null) {
   // ...
}


By checking with null with a single equal symbol, you are checking for undefined and null, as well.




Two or more depth levels
Functions should not have more than one depth level. Depth levels are done by using if, for, while and switch.
Depth levels don't apply to inner functions, object or array instantiations, ...


interface ErrorMessageArgs {
    value: number;
    upper?: number;
    lower?: number;
}

export class OutOfBoundsError extends AppError {
    private message: string = ``;

    constructor(private name: ValueName, value: number, bounds?: Bounds) {
        super();

        const lower = bounds?.lowerBoundInclusive;
        const upper = bounds?.upperBoundInclusive;

        if (lower != null) {
          this.handleTruthyLower({value, upper});
        } else {
          this.handleFalsyLower({value, upper, lower});
        }
    }

    private handleFalsyLower = (args: ErrorMessageArgs) =&gt; {
        const { value, upper } = args;
        // ...
    }

    private handleTruthyLower = (args: ErrorMessageArgs) =&gt; {
        const { value, upper, lower } = args;
        // ...
    }
}


I decide to define yet another interface to wrap the error message arguments. By doing this, I can define both functions as monadic functions (just because of Robert).


  The ideal number of arguments for a function is zero (niladic). Next comes one (monadic), followed closely by two (dyadic). Three arguments (triadic) should be avoided where possible. More than three (polyadic) requires very special justification — and then shouldn’t be used anyway.
  Robert C. Martin





message property protection
I assume that a third party which manipulates this class is not allowed to modify the error message. It doesn't make any sense, in my opinion.
That is why it may define a read-only access.


export class OutOfBoundsError extends AppError {
    private _message: string;

    constructor(private name: ValueName, value: number, bounds?: Bounds) {
        super();
        // ...
    }

    private handleFalsyLower = (args: ErrorMessageArgs) =&gt; {
        const { value, upper } = args;
        // ... message is filled
    }

    private handleTruthyLower = (args: ErrorMessageArgs) =&gt; {
        const { value, upper, lower } = args;
        // ... message is filled
    }

    get message() {
        return this._message;
    }
}


Maybe, for more consistency, _message should be protected if OutOfBoundsError is not a sealed class.
And yes, I know that in some case you defined this message property inside the AppError class. I just mention, just in case.




Ternary operation
This is just a preference. When there is only one line to write inside the if and else statements, I just prefer a ternary operation. It seems cleaner to me.


export class OutOfBoundsError extends AppError {
    protected _message = ``;

    constructor(private name: ValueName, value: number, bounds?: Bounds) {
        super();

        const lower = bounds?.lowerBoundInclusive;
        const upper = bounds?.upperBoundInclusive;

        lower != null
            ? this.handleTruthyLower({value, upper})
            : this.handleFalsyLower({value, upper, lower});
    }

    private handleFalsyLower = (args: ErrorMessageArgs) =&gt; {
        const { value, upper } = args;
        this._message = upper != null
                        ? `${this.name} must be less than or equal to ${upper} but\
                            ${value} was given`
                        : `${this.name} of ${value} is out of bounds`;
    }

    private handleTruthyLower = (args: ErrorMessageArgs) =&gt; {
        const { value, upper, lower } = args;
        this._message = upper != null
                        ? `${this.name} must be between ${lower} and ${upper} but\
                            ${value} was given`
                        : `${this.name} must be greater than or equal to ${lower}\
                            but ${value} was given`;
    }

    get message() {
        return this._message;
    }
}


Even for void function calls.

Hope it helps and any suggestions are welcome.