0

I have an interface called Bounds, and a Sprite class with a field bounds of type Bounds[][]. The Sprite's constructor has a union type argument of Bounds[]|Bounds[][]. If the argument is type Bounds[], I want to simply wrap it in an array:

class Sprite {
    constructor(bounds: Bounds[]|Bounds[][]) {
        if (!bounds) {
            this.bounds = [[ /* some default value */ ]];
        } else {
            if (!bounds.length) {
                throw new Error('Argument \'bounds\' must not be empty.');
            }

            if (!Array.isArray(bounds[0])) {
                this.bounds = [bounds];
            } else {
                this.bounds = bounds;
            }
        }
    }

    bounds: Bounds[][];
}

This code works, but the TypeScript compiler is giving me these errors for the second and third assignments, respectively:

Type '(Bounds[] | Bounds[][])[]' is not assignable to type 'Bounds[][]'.
  Type 'Bounds[] | Bounds[][]' is not assignable to type 'Bounds[]'.
    Type 'Bounds[][]' is not assignable to type 'Bounds[]'.
      Type 'Bounds[]' is not assignable to type 'Bounds'.
        Property 'x' is missing in type 'Bounds[]'.

Type 'Bounds[] | Bounds[][]' is not assignable to type 'Bounds[][]'.
  Type 'Bounds[]' is not assignable to type 'Bounds[][]'.
    Type 'Bounds' is not assignable to type 'Bounds[]'.
      Property 'length' is missing in type 'Bounds'.

How do I either explicitly tell the compiler "at this point, bounds is type Bounds[] or type Bounds[][]" or use the proper if-statements so that the compiler will arrive at this conclusion on its own?

1 Answer 1

2

If you want to help the compiler along with the reasoning, you can use a custom type guard:

const isArrayOfArrays = <T>(b: T[] | T[][]):b is T[][] => Array.isArray(b[0]);
class Sprite {
    constructor(bounds: Bounds[]|Bounds[][]) {
        if (!bounds) {
            this.bounds = [[ /* some default value */ ]];
        } else {
            if (!bounds.length) {
                throw new Error('Argument \'bounds\' must not be empty.');
            }
            if (!isArrayOfArrays(bounds)) {
                this.bounds = [bounds];
            } else {
                this.bounds = bounds;
            }
        }
    }

    bounds: Bounds[][];
}

The type guard is reusable for any such case.

The lazy way to do it is to just use a type assertion and tell the compiler what you know about the types:

class Sprite {
    constructor(bounds: Bounds[] | Bounds[][]) {
        if (!bounds) {
            this.bounds = [[ /* some default value */]];
        } else {
            if (!bounds.length) {
                throw new Error('Argument \'bounds\' must not be empty.');
            }
            if (!Array.isArray(bounds[0])) {
                this.bounds = [bounds as Bounds[]];
            } else {
                this.bounds = bounds as Bounds[][];
            }
        }
    }

    bounds: Bounds[][];
}
Sign up to request clarification or add additional context in comments.

3 Comments

That's exactly what I needed, twice over. I'm very new to TypeScript and did not know of the as and is keywords, nor how to define/use type guards.
Is there a conventional place to globally store type guards so that I don't need to call import { isArrayOfArrays } from './type-guards; in every file that uses it? Type guards seem like a good case for the global scope.
@dfoverdx I am not aware of any such conventions.. typeguards are just util functions... whatever you do with those do with type guards :)

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.