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?
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 })
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