First of all, it is impossible to do something like this in this case without extra generic parameter:
const f:Test = {
$transform: 'a',
$font: 'a',
$inherit: ['f.w', 'one.more'],
$never: 'a',
$time: 'a',
$cant: 'a', // compilation error
}
In order to infer required props for $inherit: ['f.w', 'one.more']
you should either use a function or extra generic parameter for Test
.
Something like this:
const f:Test<['f.w', 'one.more']> = {
$transform: 'a',
$font: 'a',
$inherit: ['f.w', 'one.more'],
$never: 'a',
$time: 'a',
$cant: 'a',
}
OR
declare var data: Data;
type Obj = Data
function infer<
Path extends KeysUnion<Obj>,
Inheritance extends KeysUnion<Obj> | KeysUnion<Obj>[]
>(data: Obj, path: Path, extra: Inheritance): FinalPropsInherited<Obj, Path, Inheritance>
function infer<
Path extends KeysUnion<Obj>,
Inheritance extends KeysUnion<Obj> | KeysUnion<Obj>[]
>(data: Obj, path: Path): FinalProps<Obj, Path>
function infer<
Path extends KeysUnion<Obj>,
Inheritance extends KeysUnion<Obj> | KeysUnion<Obj>[]
>(data: Data, path: Path, extra?: Inheritance) {
return null as any
}
const result = infer(data, 'l.m', ['f.w', 'one.more'])
Without extra parameter you should compute all permutations of possible $inherit
properties and object itself. It will hit recursion limit.
Here you have a solution with extra generic:
type DigInto<T extends { inherit: string, css: string }> =
T extends { inherit?: infer Inherit, css?: infer CSS }
? Inherit extends KeysUnion<Data>
? Reducer<Inherit, Data> & CSS
: Inherit extends Array<KeysUnion<Data>> ? Reducer<Inherit[number], Data> & CSS : CSS
: T
type Predicate<Accumulator extends Record<string, any>, El extends string> =
El extends keyof Accumulator ? DigInto<Accumulator[El]> : Accumulator
type Reducer<
Keys extends string,
Accumulator extends Record<string, any> = {}
> =
Keys extends `${infer Prop}.${infer Rest}`
? Reducer<Rest, Predicate<Accumulator, Prop>>
: Keys extends `${infer Last}`
? Predicate<Accumulator, Last>
: never
type BuiltIns = 'inherit' | 'css';
type KeysUnion<T, Cache extends string = ''> =
T extends PropertyKey ? Cache : {
[P in keyof T]:
P extends BuiltIns ? Cache :
P extends string
? Cache extends ''
? KeysUnion<T[P], `${P}`>
: Cache | KeysUnion<T[P], `${Cache}.${P}`>
: never
}[keyof T]
type Data = {
foo: {
bar: {
css: Record<"$color", string> & Record<"$margin", string>
}
biz: {
// inherits the props from foo.bar
inherit: 'foo.bar'
css: Record<"$what", string>
}
},
two: {
three: {
// inherits the props from foo.biz
inherit: 'foo.biz',
css: Record<"$padding", string>
}
}
c: {
bar: {
// inherits the props from two.three
inherit: 'two.three'
}
},
d: {
foo: {
css: Record<"$never", string>
}
},
e: {
q: {
// inherits the props from d.foo
inherit: "d.foo"
}
},
f: {
w: {
// inherits the props from both e.q and c.bar
inherit: ["e.q", "c.bar"]
}
}
h: {
i: {
// Inherits the props from c.bar
inherit: 'c.bar',
css: Record<"$font", string>
}
},
l: {
m: {
// Inherits the props from h.i
inherit: 'h.i',
css: Record<"$transform", string> & Record<"$temp", string>
}
}
x: {
x: {
// inherits the props f.w
inherit: 'f.w'
}
}
one: {
more: {
css: Record<"$time", string>
}
}
another: {
branch: {
css: Record<"$cant", string>
}
}
}
type UnionKeys<T> = T extends T ? keyof T : never;
//https://stackoverflow.com/questions/65805600/type-union-not-checking-for-excess-properties#answer-65805753
type StrictUnionHelper<T, TAll> =
T extends any
? T & Partial<Record<Exclude<UnionKeys<TAll>, keyof T>, never>> : never;
type StrictUnion<T> = StrictUnionHelper<T, T>
type ExtraProperties = Record<'$transform', string> & Record<'$temp', string>
type MakePartial<T> = StrictUnion<Partial<T>>
type InheritedProps<Obj, Props extends KeysUnion<Obj>> = MakePartial<Reducer<Props, Obj>>
type FinalProps<
Obj,
Props extends KeysUnion<Obj>,
> = InheritedProps<Obj, Props> & MakePartial<ExtraProperties>
// type Result = {
// $color?: string | undefined;
// $margin?: string | undefined;
// $what?: string | undefined;
// $padding?: string | undefined;
// $font?: string | undefined;
// $transform?: string | undefined;
// $temp?: string | undefined;
// }
type Result = FinalProps<Data, 'l.m'>
type FinalPropsInherited<
Obj,
Props extends KeysUnion<Obj>,
Inheritance extends KeysUnion<Obj> | KeysUnion<Obj>[]
> =
Inheritance extends KeysUnion<Obj>
? FinalProps<Obj, Props> & Record<'$inherit', Inheritance> & Reducer<Inheritance, Obj>
: Inheritance extends KeysUnion<Obj>[]
? FinalProps<Obj, Props> & Record<'$inherit', Inheritance> & Reducer<Inheritance[number], Obj>
: never
const base: Result = { // SHOULD BE OK
$transform: 'a',
$font: 'a',
}
const a: Result = { // $never was not a prop defined or inherited by l.m so this is an error
$transform: 'a',
$font: 'a',
$never: 'a',
}
const b: FinalPropsInherited<Data, 'l.m', 'f.w'> = { // We now are saying $inherit f.w which eventually adds a $never and makes this now OK
$transform: 'a',
$font: 'a',
$inherit: 'f.w',
$never: 'a',
}
const c: FinalPropsInherited<Data, 'l.m', 'f.w'> = { // $time is not ever added yet, so this is an error
$transform: 'a',
$font: 'a',
$inherit: 'f.w',
$time: 'a'
}
const d: FinalPropsInherited<Data, 'l.m', 'one.more'> = { // We are now saying $inherit one.more which does add the $time property and this is OK
$transform: 'a',
$font: 'a',
$inherit: 'one.more',
$time: 'a', // ok
}
const e: FinalPropsInherited<Data, 'l.m', ['f.w', 'one.more']> = { // shoule be OK
$transform: 'a',
$font: 'a',
$inherit: ['f.w', 'one.more'],
$never: 'a',
$time: 'a',
}
const f: FinalPropsInherited<Data, 'l.m', ['f.w', 'one.more']> = { // shoule be ERROR no $cant ---- (but it is allowing $cant even tho `l.m | f.w | one.more` none of which add a $cant prop)
$transform: 'a',
$font: 'a',
$inherit: ['f.w', 'one.more'],
$never: 'a',
$time: 'a',
$cant: 'a',
}
declare var data: Data;
type Obj = Data
function infer<
Path extends KeysUnion<Obj>,
Inheritance extends KeysUnion<Obj> | KeysUnion<Obj>[]
>(data: Obj, path: Path, extra: Inheritance): FinalPropsInherited<Obj, Path, Inheritance>
function infer<
Path extends KeysUnion<Obj>,
Inheritance extends KeysUnion<Obj> | KeysUnion<Obj>[]
>(data: Obj, path: Path): FinalProps<Obj, Path>
function infer<
Path extends KeysUnion<Obj>,
Inheritance extends KeysUnion<Obj> | KeysUnion<Obj>[]
>(data: Data, path: Path, extra?: Inheritance) {
return null as any
}
const result = infer(data, 'l.m', ['f.w', 'one.more'])
Playground
Please let me know if it works for you. If it does - I will provide some explanation.
Example of usage with React provided by OP - @JD Isaacks
type SC<I extends KeysUnion<Data> | KeysUnion<Data>[]> = StyledComponent<"div", any, FinalPropsInherited<Data, 'l.m', I>>
type MyComp<I extends KeysUnion<Data> | KeysUnion<Data>[]> = ReturnType<SC<I>>
const D = <I extends KeysUnion<Data> | KeysUnion<Data>[],>(props: FinalPropsInherited<Data, 'l.m', I>): MyComp<I> => null as any
const Comp = () => {
return (
<D $inherit={['f.w', 'one.more', 'another.branch']} $transform="sd" $never="sd" $time="sd" $cant="sd" />
)
}