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
P.S.S. Here you can find related answer and here is my article