Home > Software engineering >  Unable to define type for tuple: Target requires 2 element(s) but source may have fewer
Unable to define type for tuple: Target requires 2 element(s) but source may have fewer

Time:09-07

I'm trying to populate an array consisting of tuples

const countries = ['sg', 'my', 'th'];
const platforms = ['ios', 'android'];

const combinationsToQuery = platforms.flatMap((platform) =>
    countries.map((cid) => [platform, cid])
); // [["ios", "sg"], ["ios", "my"], ["ios", "th"], ["android", "sg"], ["android", "my"], ["android", "th"]]

Now I'm trying to add some types to them and below is my attempt

type country = 'sg' | 'my' | 'th' | 'uk' | 'us'
type platform = 'ios' | 'android'

const countries: country[] = ['sg', 'my'];
const platforms: platform[] = ['ios', 'android'];

const combinationsToQuery: [platform, country][] = platforms.flatMap((platform) =>
    countries.map((cid: country) => [platform, cid])
);

Type '(country | platform)[][]' is not assignable to type '[platform, country][]'. Type '(country | platform)[]' is not assignable to type '[platform, country]'. Target requires 2 element(s) but source may have fewer.(2322)

If I don't specifically define [platform, country][], TS will infer combinationsToQuery to be (country | platform)[][], which doesn't sounds right either?

TS Playground

CodePudding user response:

The problem becomes more obvious if you look at the return type for the .map callback. If you tweak your original code to:

countries.map((cid: country) => {
  const result = [platform, cid];
  return result;
})

Above, result is typed as (country | platform)[] - not as a tuple containing two elements, but as an array whose (any number of) elements are either platform or country. (So, the resulting type of the whole combinationsToQuery isn't right either.)

You can either assert the type returned by the callback is a tuple:

const combinationsToQuery: [platform, country][]  = platforms.flatMap((platform) =>
    countries.map((cid) => [platform, cid] as [platform, country])
);

Or declare your arrays as const and the return value from the callback as const and let TypeScript infer the rest for you.

const countries = ['sg', 'my'] as const;
const platforms = ['ios', 'android'] as const;

const combinationsToQuery = platforms.flatMap((platform) =>
    countries.map((cid) => [platform, cid] as const)
);

CodePudding user response:

First of all, there is a convention in TS, that you need to Capitalize all type names, otherwise, it is hard to read.

As for this type:

type country = 'sg' | 'my' | 'th' | 'uk' | 'us'
type platform = 'ios' | 'android'

const countries: country[] = ['sg', 'my'];
const platforms: platform[] = ['ios', 'android'];

Using country[] type for countries const is not safe, because it allows you to use duplicates:

const countries: country[] = ['sg', 'sg']

Hence, we need to use smth more useful:

type Country = 'sg' | 'my' | 'th'
type Platform = 'ios' | 'android'

// https://github.com/microsoft/TypeScript/issues/13298#issuecomment-692864087
type TupleUnion<U extends string, R extends any[] = []> = {
    [S in U]: Exclude<U, S> extends never ? [...R, S] : TupleUnion<Exclude<U, S>, [...R, S] | R>;
}[U];


type Countries = TupleUnion<Country>
type Platforms = TupleUnion<Platform>

const countries: Countries = ['sg', 'my'];

const platforms: Platforms = ['ios', 'android'];

const should_be_error : Platforms = ['ios', 'ios'] // error

As you might have noticed, Countries and Platforms are combination of all possible/allowed values.

So, now, we have to apply each platform to each country. Or in other words, we need a permutation of these two sets.

COnsider this example:

type Country = 'sg' | 'my' | 'th'
type Platform = 'ios' | 'android'

// https://github.com/microsoft/TypeScript/issues/13298#issuecomment-692864087
type TupleUnion<U extends string, R extends any[] = []> = {
    [S in U]: Exclude<U, S> extends never ? [...R, S] : TupleUnion<Exclude<U, S>, [...R, S] | R>;
}[U];


type Countries = TupleUnion<Country>
type Platforms = TupleUnion<Platform>

const countries: Countries = ['sg', 'my'];

const platforms: Platforms = ['ios', 'android'];

type Add<Plat extends string, Cntrs extends string[]> = {
    [Index in keyof Cntrs]: [Plat, Cntrs[Index]]
}

type Compute<
    Plats extends string[],
    Cntrs extends string[],
    Acc extends string[][] = []
> =
    /**
     * If last call of recursion
     */
    (Plats extends []
        /**
         * Return Accumulator
         */
        ? Acc
        /**
         * If this is not the last step, infer first and rest elements
         */
        : (Plats extends [
            infer Head extends string,
            ...infer Rest extends string[]
        ]
            /**
             * Call Compute recursively using Rest as a main tuple (Platforms)
             * and call our Add "callback", which just adds Head element to each Country
             */
            ? Compute<Rest, Cntrs, [...Acc, ...Add<Head, Cntrs>]>
            : Acc)
    )

function combinations<P extends Platforms, C extends Countries>(platforms: P, countries: C): Compute<P, C>
function combinations<P extends Platforms, C extends Countries>(platforms: P, countries: C) {
    return platforms.flatMap((platform) =>
        countries.map((cid) => [platform, cid])
    )
}

// [["ios", "sg"], ["ios", "my"], ["android", "sg"], ["android", "my"]]
const result = combinations(platforms, countries)

console.log({ result })

Playground

As you might have noticed, Compute type utility does the same thing as your runtime code with one difference - it uses recursion instead of regular array iteration.

Also, there is no need to use as const assertion

  • Related