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.

Terry
  • 63,248
  • 15
  • 96
  • 118
  • Is it feasible to rather use a custom type guard? https://www.typescriptlang.org/play/#code/C4TwDgpgBAwgrgZ2AewLYEEBOmCGIoC8UWuIAPAHZyoBGEmUAPlEpgJYUDmAfANwBQ-AGZwKAY2BtkFKGwTwkaEngAUOTJwBcUHBRABKbes6yEsRCgzY8UAN78ojqJgjA4mGcpAA6OV7Ua+lAAZMFQASY4ZrogANoAuvreCGgQKgwE3FCgkMhC6UEERVAA5KwcnCX6AgC+gvxi0kg62gqWXoRQsSVCyMglADRQAIxDAExDAMzxAo0UzTStFkrW+ESxo1ATpb39UzMNTcBQYkuKVqSd3bslB4fzxwAmZ+2rV8N3bELhcm0rpGp9EF7E4Tk1kAAbCDeCHITgqEo4Uw6cznLxVWr8L4-eTLC6qR5AuwOJxzFJQmFwhGPZFIv74kAY-h1CAQhDQEGk8EU2Hwko0uRQAByAHkACoo+no6rMoA – briosheje Jul 29 '19 at 08:09
  • Related: https://stackoverflow.com/questions/49910889/typescript-array-with-minimum-length – k0pernikus Jul 29 '19 at 10:07

4 Answers4

2

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.

Cerbrus
  • 70,800
  • 18
  • 132
  • 147
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);
KooiInc
  • 119,216
  • 31
  • 141
  • 177
  • Then why wouln't you just use `CustomArray` instead of `MyArray`? – Cerbrus Jul 29 '19 at 09:28
  • `arr as MyArray` would make sense then. Also returning an error is really ugly – Jonas Wilms Jul 29 '19 at 09:42
  • 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 Jul 29 '19 at 09:54
  • @Terry, no problem. I must confess that I seldom feel the need to use typescript. – KooiInc Jul 29 '19 at 15:00
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
k0pernikus
  • 60,309
  • 67
  • 216
  • 347
  • 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 Jul 29 '19 at 09:54
  • 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? – Rax Adaam Jul 31 '23 at 16:47
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
];
k0pernikus
  • 60,309
  • 67
  • 216
  • 347