3

Let's say that I want to enforce strict typing on a given array, such that:

  • It may contain any number (a.k.a. zero or more) of number
  • It must contain one number or more of string

...and the order of number and string in the array does not matter, so that the following arrays are valid:

  • ['foo', 1, 2, 3]
  • [1, 2, 'foo', 3]
  • ['foo']

But the following array is invalid:

  • [1, 2, 3] (because it needs at least one string in the array)

The closest solution that almost worked for me is to define the type as:

/**
 * Array must contain:
 * - One or more string
 * - Zero or more numbers
 * @type
 */
type CustomArray = [string] & Array<number | string>

const a: CustomArray = ['foo', 1, 2, 3]; // Should pass (works as expected)
const b: CustomArray = [1, 2, 'foo', 3]; // Should pass (but doesn't with my code)
const c: CustomArray = ['string'];       // Should pass (works as expected)
const d: CustomArray = [1, 2, 3];        // Should fail (works as expected)

But this means that the first element of the array must be a string, instead of enforcing a minimum count of 1 throughout the array. You can test it out on TypeScript Playround here.

2

4 Answers 4

3

There's no way to tell TypeScript that an array should contain at least one element of a specific type.

The best you can do is just create an number / string array:

type CustomArray = (number | string)[];

Or

type CustomArray = Array<number | string>;

And then add the required checks where you're adding or reading data to / from the array.

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

4 Comments

How about adding a & { _isCustomArray: true }, then typecast with a check.
@JonasWilms: how would that validate the requirement of having at least one string entry?
function asCustomArray(array: (string | number)[]): CustomArray | never { ... }
.... or just using a custom type guard?....Though that won't do a strict type check before runtime.
0

Would this be an idea?

/**
 * Array must contain:
 * - One or more string
 * - Zero or more numbers
 * @type
 */
console.clear();

const CustomArray = (arr: Array<string | number>) => {
  if (!arr.length) {
    return new TypeError("Invalid: array should contain at least one element");
  }

  const oneString = arr.filter(v => v.constructor === String).length;
  const wrongTypes = arr.filter(v => v.constructor !== Number && v.constructor !== String).length && true || false;
  
  if (wrongTypes) {
    return new TypeError(`Invalid: [${JSON.stringify(arr)}] contains invalid types`);
  }

  if (!oneString) {
    return new TypeError(`Invalid: [${JSON.stringify(arr)}] should contain at least one String value`);
  }
  
  return arr;
};

type MyArray = Array<string | number> | TypeError;

const a: MyArray = CustomArray(['foo', 1, 2, 3]); // Should pass (works as expected)
const b: MyArray = CustomArray([1, 2, 'foo', 3]); // Should pass (works as expected)
const c: MyArray = CustomArray(['string']);       // Should pass (works as expected)
const d: MyArray = CustomArray([1, 2, 3]);        // Should fail (works as expected)

const log = (v:any) => 
  console.log(v instanceof TypeError ? v.message : JSON.stringify(v));

log(a);
log(b);
log(c);
log(d);

4 Comments

Then why wouln't you just use CustomArray instead of MyArray?
arr as MyArray would make sense then. Also returning an error is really ugly
Thanks for the answer! However, I'm not looking for a function that does that check: just a type definition that fulfills the requirements. Looks like it's not possible right now.
@Terry, no problem. I must confess that I seldom feel the need to use typescript.
0

There might be one option to force a minimum size of an array, yet it only works well with a tupled approach:

type OneOrMore<T> = {
    0: T
} & T[]
type Custom = [number[], OneOrMore<string>];

const bar: Custom = [
    [
        1,
        2,
        3
    ],
    ["bar"],
];

const foo: Custom = [
    [
        1,
        2,
        3
    ],
    [], // missing zero property
];

Comments

0

There appears to be no way to have a compile-time check for an array having a minimum size of a specific type.


What follows is a runtime-check-based solution:

Instead of having an array of different types, I would introduce a tuple of strings and numbere.

type Custom = [number[], string[]];

You then can define a check function to see if it fulfills your requirement:

const isValid = (c: Custom): boolean => {
    const [_, strings] = c;

    return strings.length >= 1;
};

If you need your inputs as an array, you can transform them into tuples:

const toCustom = (a: (string | number)[]): Custom => {
    const numbers: number[] = [];
    const strings: string[] = [];

    a.forEach((element) => {
        if (typeof element === "string") {
            strings.push(element);
        } else if (typeof element === "number") {
            numbers.push(element);
        } else {
            throw new Error("Unexpected type given"); // for runtime errors
        }
    });

    return [
        numbers,
        strings
    ];
}

And then you can enforce runtime checks:

const a = isValid(toCustom([
    "foo",
    1,
    2,
    3
])); // Should pass
const b = isValid(toCustom([
    1,
    2,
    "foo",
    3
])); // Should pass

const c = isValid(toCustom(["string"]));       // Should pass 
const d = isValid(toCustom([
    1,
    2,
    3
])); // should fail

console.log(a, b, c, d); // prints: true true true false

2 Comments

Thanks for the answer! However, I'm not looking for a function that does that check: just a type definition that fulfills the requirements. Looks like it's not possible right now.
Re: "There appears to be no way to have a compile-time check for an array having a minimum size of a specific type." Is this based on an open issue / ticket from TS? Or just your conclusion based on looking around?

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.