Please let me know if it works for you:
interface Entity {
a: string;
b: number;
c: boolean;
}
type Column<Key, Arg> = {
keys: Key,
render: (arg: Arg) => string
}
type Keys = 'a' | ['a', 'c'];
// credits goes to https://stackoverflow.com/a/50375286
type UnionToIntersection<U> = (U extends any ? (k: U) => void : never) extends (
k: infer I
) => void
? I
: never;
// credits goes to https://github.com/microsoft/TypeScript/issues/13298#issuecomment-468114901
type UnionToOvlds<U> = UnionToIntersection<
U extends any ? (f: U) => void : never
>;
// credits goes to https://github.com/microsoft/TypeScript/issues/13298#issuecomment-468114901
type PopUnion<U> = UnionToOvlds<U> extends (a: infer A) => void ? A : never;
// credits https://stackoverflow.com/users/125734/titian-cernicova-dragomir
type IsUnion<T> = [T] extends [UnionToIntersection<T>] ? false : true;
type UnionToArray<T, A extends unknown[] = []> = IsUnion<T> extends true
? UnionToArray<Exclude<T, PopUnion<T>>, [PopUnion<T>, ...A]>
: [T, ...A];
type ReducerElem = string;
type Reducer<
Arr extends ReadonlyArray<ReducerElem>,
Result extends Record<string, any> = {}
> = Arr extends []
? Result
: Arr extends [infer H]
? H extends ReducerElem
? Result & Record<H, H>
: never
: Arr extends readonly [infer H, ...infer Tail]
? Tail extends ReadonlyArray<ReducerElem>
? H extends ReducerElem
? Reducer<Tail, Result & Record<H, H>>
: never
: never
: never;
type MapPredicate<T> =
T extends string
? Column<T, T>
: T extends Array<string>
? Column<T, Reducer<T>>
: never
type Mapped<
Arr extends Array<unknown>,
Result extends Array<unknown> = []
> = Arr extends []
? []
: Arr extends [infer H]
? [...Result, MapPredicate<H>]
: Arr extends [infer Head, ...infer Tail]
? Mapped<[...Tail], [...Result, MapPredicate<Head>]>
: Readonly<Result>;
type Result = Mapped<UnionToArray<'a' | ['a', 'c']>>;
const columns: Result = [
{
keys: 'a',
render: a => a,
},
{
keys: ['a', 'c'],
render: ({ a, c }) => c ? a : 'something else',
}
]
If you agree with this solution I will provide more explanation.
Apart from that, here and here , in my blog, you can find some exmplanation
Playground
This is js analogue of Reducer
utility type
const reducer = (arr: ReadonlyArray<Elem>, result: Record<string, any> = {}): Record<string, any> => {
if (arr.length === 0) {
return result
}
const [head, ...tail] = arr;
return reducer(tail, { ...result, [head]: 'string' })
}
UPDATE
You can use helper function, but there is drawback. You still have to provide explicit type. Good news - TS will complain if explicit type is not allowed.
interface Data {
a: string;
b: number;
c: boolean;
}
type Column<Key, Arg> = {
keys: Key,
render: <T extends Arg>(arg: T) => string
}
// credits goes to https://stackoverflow.com/a/50375286
type UnionToIntersection<U> = (U extends any ? (k: U) => void : never) extends (
k: infer I
) => void
? I
: never;
// credits goes to https://github.com/microsoft/TypeScript/issues/13298#issuecomment-468114901
type UnionToOvlds<U> = UnionToIntersection<
U extends any ? (f: U) => void : never
>;
// credits goes to https://github.com/microsoft/TypeScript/issues/13298#issuecomment-468114901
type PopUnion<U> = UnionToOvlds<U> extends (a: infer A) => void ? A : never;
// credits https://stackoverflow.com/users/125734/titian-cernicova-dragomir
type IsUnion<T> = [T] extends [UnionToIntersection<T>] ? false : true;
type UnionToArray<T, A extends unknown[] = []> = IsUnion<T> extends true
? UnionToArray<Exclude<T, PopUnion<T>>, [PopUnion<T>, ...A]>
: [T, ...A];
type ReducerElem = string;
type Predicate<T> = T extends keyof Data ? Record<T, Data[T]> : never
type Reducer<
Arr extends ReadonlyArray<ReducerElem>,
Result extends Record<string, any> = {}
> = Arr extends []
? Result
: Arr extends [infer H]
? H extends ReducerElem
? Result & Predicate<H>
: never
: Arr extends readonly [infer H, ...infer Tail]
? Tail extends ReadonlyArray<ReducerElem>
? H extends ReducerElem
? Reducer<Tail, Result & Predicate<H>>
: never
: never
: never;
type MapPredicate<T> =
T extends string
? Column<T, T>
: T extends Array<keyof Data>
? Column<T, Reducer<T>>
: never
type Mapped<
Arr extends Array<unknown>,
Result extends Array<unknown> = []
> = Arr extends []
? []
: Arr extends [infer H]
? [...Result, MapPredicate<H>]
: Arr extends [infer Head, ...infer Tail]
? Mapped<[...Tail], [...Result, MapPredicate<Head>]>
: Readonly<Result>;
type Format<Keys extends ReadonlyArray<keyof Data>> =
UnionToArray<Keys[number]> extends ReadonlyArray<string>
? Reducer<UnionToArray<Keys[number]>>
: never
interface Union<Keys extends keyof Data> {
keys: Keys,
render: (a: Data[Keys]) => void
}
interface WithArray<Keys extends ReadonlyArray<keyof Data>> {
keys: Keys,
render: (a: Format<Keys>) => void
}
type Base<Keys> = Keys extends Array<keyof Data> ? WithArray<Keys> : Keys extends keyof Data ? Union<Keys> : any
const builder = <V extends Array<keyof Data>, U extends keyof Data>(columns: [...(Base<[...V]> | Base<U>)[]]) => columns
const result = builder([{
keys: ['c'], render: (prop: Record<"c", boolean>) => prop
}, {
keys: 'b', render: (prop) => prop
}])
Playground
You can also make union of all possible values. Smth similar like I did here. But if your Entity
interface contains more than 5 props, it will not work.
Or, you can use helper for each entity:
interface Data {
a: string;
b: number;
c: boolean;
}
type Column<Key, Arg> = {
keys: Key,
render: <T extends Arg>(arg: T) => string
}
// credits goes to https://stackoverflow.com/a/50375286
type UnionToIntersection<U> = (U extends any ? (k: U) => void : never) extends (
k: infer I
) => void
? I
: never;
// credits goes to https://github.com/microsoft/TypeScript/issues/13298#issuecomment-468114901
type UnionToOvlds<U> = UnionToIntersection<
U extends any ? (f: U) => void : never
>;
// credits goes to https://github.com/microsoft/TypeScript/issues/13298#issuecomment-468114901
type PopUnion<U> = UnionToOvlds<U> extends (a: infer A) => void ? A : never;
// credits https://stackoverflow.com/users/125734/titian-cernicova-dragomir
type IsUnion<T> = [T] extends [UnionToIntersection<T>] ? false : true;
type UnionToArray<T, A extends unknown[] = []> = IsUnion<T> extends true
? UnionToArray<Exclude<T, PopUnion<T>>, [PopUnion<T>, ...A]>
: [T, ...A];
type ReducerElem = string;
type Predicate<T> = T extends keyof Data ? Record<T, Data[T]> : never
type Reducer<
Arr extends ReadonlyArray<ReducerElem>,
Result extends Record<string, any> = {}
> = Arr extends []
? Result
: Arr extends [infer H]
? H extends ReducerElem
? Result & Predicate<H>
: never
: Arr extends readonly [infer H, ...infer Tail]
? Tail extends ReadonlyArray<ReducerElem>
? H extends ReducerElem
? Reducer<Tail, Result & Predicate<H>>
: never
: never
: never;
type MapPredicate<T> =
T extends string
? Column<T, T>
: T extends Array<keyof Data>
? Column<T, Reducer<T>>
: never
type Mapped<
Arr extends Array<unknown>,
Result extends Array<unknown> = []
> = Arr extends []
? []
: Arr extends [infer H]
? [...Result, MapPredicate<H>]
: Arr extends [infer Head, ...infer Tail]
? Mapped<[...Tail], [...Result, MapPredicate<Head>]>
: Readonly<Result>;
type Format<Keys extends ReadonlyArray<keyof Data>> =
UnionToArray<Keys[number]> extends ReadonlyArray<string>
? Reducer<UnionToArray<Keys[number]>>
: never
function maker<K extends Array<keyof Data>>(keys: K, cb: (a: Format<K>) => void): { keys: K, render: (a: Format<K>) => void }
function maker<K extends keyof Data>(keys: K, cb: (a: Data[K]) => void): { keys: K, render: (a: Data[K]) => void }
function maker(keys: keyof Data | Array<keyof Data>, cb: (...args: any[]) => void) {
return {
keys,
render: cb
}
}
const COLUMNS = [
maker('a', (prop /** stirng */) => { }),
maker(['a'], (prop /** Record<"a", string> */) => { }),
maker('b', (prop /** Record<"a", string> */) => { })
] as const
Playground