0

I'm confused by nested array behavior in TypeScript. I want to use a single type for unknown-depth arrays.

Here is a mockup of my problem:

type PossiblyNested = Array<number> | Array<Array<number>>; // Limited to 2D, for simplicity. My actual input will be up to 6-dimensional.

const FLAT_INPUTS: PossiblyNested = [0, 1];
const NESTED_INPUTS: PossiblyNested = [[0], [1]];

const PROCESSED_INPUTS: PossiblyNested = [];

for (let i = 0; i < FLAT_INPUTS.length; ++i) {
    const RAW = FLAT_INPUTS.shift();

    PROCESSED_INPUTS.push(RAW); //  Argument of type 'number' is not assignable to parameter of type 'number & number[]'.
                                //  Type 'number' is not assignable to type 'number[]'.ts(2345) 
}

for (let i = 0; i < NESTED_INPUTS.length; ++i) {
    const RAW = NESTED_INPUTS.shift();

    PROCESSED_INPUTS.push(RAW); //  Argument of type 'number[]' is not assignable to parameter of type 'number & number[]'.
                                //  Type 'number[]' is not assignable to type 'number'.ts(2345)
}

I have tried using generic templated types, and recursive types, to no avail. I might not understand those well enough.

I have been able to satisfy TS and Lint with 'any' and 'unknown', but that leads to other problems. I want to avoid those at all costs.

I have had success initializing PROCESSED_INPUTS with items of the expected format (see next example), but I then must manually empty it before the loop. It feels very clunky.

const PROCESSED_INPUTS_FLAT: PossiblyNested = [0]; // include content

PROCESSED_INPUTS_FLAT.pop(); // manually empty

for (let i = 0; i < FLAT_INPUTS.length; ++i) {
    const RAW = FLAT_INPUTS.shift();

    PROCESSED_INPUTS_FLAT.push(RAW); // TS and Lint are now happy
}

Is there a cleaner way to initialize PROCESSED_INPUTS so that it takes pushed entries dynamically, whether they are number or number[]? I hoped that's what I was setting up with my PossiblyNested type ... apparently not.

Thanks in advance for any help!

1 Answer 1

1

Your PossiblyNested type means either an array of only numbers, or a two-dimesional array of only numbers. When TypeScript calculates the PROCESSED_INPUTS.push, it uses intersection types for the contravariant parameters, so the type of that method becomes (...items: (number & number[])[]) if the array is an Array<number>, which accepts number in push, or an Array<Array<number>>, which accepts Array<number>.

To fix this, you can make PROCESSED_INPUTS's type Array<number | Array<number>>, which is an array of either numbers or an array of numbers.

const PROCESSED_INPUTS: Array<number | Array<number>> = [];

// ...

for (let i = 0; i < FLAT_INPUTS.length; ++i) {
    const RAW = FLAT_INPUTS.shift();

    PROCESSED_INPUTS.push(RAW!); // need non-null assertion because RAW could be undefined
}

Playground link

Sign up to request clarification or add additional context in comments.

2 Comments

Thanks for the insight! However, as I understand this, PossiblyNested would then allow mixed-depth arrays, e.g. "const MIXED: PossiblyNested = [0, [1]];" would be allowed. I need to keep those illegal, in this project. Do you know of a way to enforce that in the type definition? Thanks again.
Ok, I reviewed the edit. It looks like I can still assign a mixed-depth array ([0, [1]]) using this type definition. I'm hoping to require that all numbers be at the same depth. But your answer did solve my immediate problem, which was the union vs intersection handling by array.push(). Thanks again!!

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.