Flattening an array of arrays to their base types in TypeScript.

TypeScript has an incredibly powerful and flexible type system that seems to be improving at a breakneck pace every couple of weeks. The language’s conditional types that were introduced in TypeScript 2.8 open a ton of doors with the type system. For instance, you can finally write a function that will return one of two different types depending on the input, and have the proper type for the output rather than a union of the possible output types.

Before conditional types were introduced in TypeScript 2.8 you’d have to write that function like this:

type Output = number[] | number;

// If this function receives a string it will return a number array, else it will return a number.
function myFunc(input: number | string): Output {
  if (typeof input === "string") {
    return [1,2,3]
  }
  
  return input;
}

const result = myFunc(15) // Type is number[] | number and we need to use type guards to determine which

In TypeScript 2.8 and later, you can now tell TypeScript that if the function’s receives a string it will output a number array, else it will output a number:

// If T is a string then the type is a number array, else it's a number.
type Output<T> = T extends string ? number[] : number;

function myFunc<T extends string | number>(input: T): Output<T> {
    if (typeof input === "string") {
        return [1, 2, 3] as Output<T>;
    }

    return input as Output<T>;
}

const a: number[] = myFunc("hello");
const b: number = myFunc(5);
const c: number = myFunc("test"); // Error! Type 'number[]' is not assignable to type 'number'.

Already it should be possible to see how valuable the conditional type system is, especially when writing TypeScript declaration files for a package that was written in JavaScripts (where a function may have a slew of different output types depending on the input, or even different input types depending on preceding input types!

To get to the point, I recently needed to write a function whose first argument was either a string, an array of strings, or an array of an array of strings. The second argument then had to be one of those specific string values. To put it more simply, if the first arg’s value was ["Hello", ["World"], ["Foo", "Bar"]], then the second argument had to be either "Hello", "World", "Foo" or "Bar".

Before conditional types it wouldn’t have been possible to write that requirement “dynamically” — you’d have to know each possible value of the array to do so. Now with conditional types we can flatten the first argument down to an array of all strings contained within:

// This type flattens an array down to its component strings. 
type Flattened<T extends string | Array<string> | Array<Array<string>>> = T extends string ? T : T extends Array<Array<string>> ? T[number][number] : T[number]

function myFunc<T extends string | Array<string> | Array<Array<string>>>(choices: T, choice: Flattened<T>) {
    // Do something with the choice
}

const input = ["hello", ["world"], ["foo", "bar"]]

myFunc(input, "hello"); // Works because "hello" is one of the strings in the input array
myFunc(input, "something"); // Fails! "something" is not assignable to type "hello" | "world" | "foo" | "bar"

And that’s all it takes to pluck out the individual values in the input array and use them as a type in the next argument. You could also use the type for the function’s return, say if you were writing a function that returns one of those values:

function getRandomValue<T extends string | Array<string> | Array<Array<string>>>(choices: T): Flattened<T> {
    // Get a random value from the input and return it
}

const choice: Flattened<typeof input> = getRandomValue(input) // Type is "hello" | "world" | "foo" | "bar"

Awesome! However, for just a tiny bit of feedback on this incredibly useful new feature, I do wish conditional types could be applied to non-generic arguments:

function getRandomValue(input: string | Array<string> | Array<Array<string>>, choice: Flattened<typeof input>) {
    // Actually this might really work lol, need to investigate
}

Learn how to build rock solid Shopify apps with C# and ASP.NET!

Did you enjoy this article? I wrote a premium course for C# and ASP.NET developers, and it's all about building rock-solid Shopify apps from day one.

Enter your email here and I'll send you a free sample from The Shopify Development Handbook. It'll help you get started with integrating your users' Shopify stores and charging them with the Shopify billing API.

We won't send you spam. Unsubscribe at any time.