1

Say I have a generic type that takes a parameter:

type AnimalProps<T> = T extends any ? /* do something specific based on T */ : never

Then say I want to alter a type with AnimalProps based on a specific property of said type:

type AnimalLookup<S> = AnimalProps<S> extends any ? Record<"kind", S> & AnimalProps<S> : never

type AnimalStructure = Partial<
  Record<"name",string> & 
  AnimalLookup<string>
>

I want to then be able to do:

const obj:AnimalStructure = {
  name: "Fred The Snake",
  kind: "snake", // <--"snake" is passed to AnimalProps<"snake"> and alters the type of `obj`
  slither: "fast", // <-- property added via AnimalProps<"snake">
}

I was able to achieve this, by doing something like:

type AnimalKind = "snake" | "deer" | ...
type AnimalLookup<S extends AnimalKind> = AnimalProps<S> extends any ? Record<"kind", S> & AnimalProps<S> : never
type AnimalStructure = Partial<
  Record<"name",string> & 
  AnimalLookup<AnimalKind>
>

However the part I cannot seem to figure out is extending this to be an array of kinds:

const obj:AnimalStructure = {
  name: "Albert The Snake who is also a Duck",
  kind: ["snake", "duck"], // <-- passed to AnimalProps<"snake | duck"> and alters the type of `obj`
  slither: "slow", // <-- property added via AnimalProps<"snake | duck">
  fly: "fast", // <-- property added via AnimalProps<"snake | duck">
}

If I try defining my animal structure like so:

type Walk<K extends AnimalKind[], Cache = {}> =
    K extends [infer Single]
      ? Single extends AnimalKind
        ? AnimalProps<Single> & Cache
        : K extends [infer Next, ...infer Rest]
          ? Rest extends AnimalKind[]
            ? Next extends AnimalKind
              ? Walk<Rest, AnimalProps<Next> & Cache> 
              : never
            : never
          : never
      : never

type AnimalLookup<S extends AnimalKind[]> = Walk<S> extends any ? Record<"kind", S> & Walk<S> : never

type AnimalStructure = Partial<
  Record<"name",string> & 
  AnimalLookup<AnimalKind[]>

I have tried several things and I am only able to get all or nothing meaning, I add an array of animal kinds to my object and either NO animal props are allowed or ALL props for ALL animals are allowed (not just the ones I specified).

const obj:AnimalStructure = {
  name: "Albert The Snake who is also a Duck",
  kind: ["snake", "duck"],
  slither: "slow",
  fly: "fast",
  climb: "trees", <-- this ends up being allowed, because it is an animal prop but not for snake or duck.
}

This is a contrived example, of a more complex problem that is likely out of scope but here is playground link to a more concrete example. The bottom shows a series of structures that I expect to be valid types or invalid types. All of them are passing except the final one.

JD Isaacks
  • 56,088
  • 93
  • 276
  • 422
  • It is a bit unclear for me. Could you please provide your final solution and comment what you expect and what you have. This is a good question, I will try to help you – captain-yossarian from Ukraine Aug 10 '21 at 13:18
  • @captain-yossarian please see my added comment with link to playground. It has all my attempts. There are a series of structures on the bottom to test the solution. Some should be invalid and others should be valid (comments explain). All are as expected except for the last one. – JD Isaacks Aug 10 '21 at 13:30
  • there syntax errors in your code. Also, could you please reduce this example to minimum? It is very hard to get around with such a huge example. – captain-yossarian from Ukraine Aug 10 '21 at 13:37
  • 1
    @captain-yossarian I updated my link to a new playground with all other stuff removed and only showcasing a single attempt. The only errors should be on the bottom when I am showing examples that SHOULD error. But the last example should also error, but does not. I explain why. – JD Isaacks Aug 10 '21 at 13:54
  • @captain-yossarian I went ahead and started a bounty if you are anyone is able to solve this. Starting to think it might not be possible? – JD Isaacks Aug 12 '21 at 12:37
  • As far as I understood you have a type `T` -> `foo:{}, two:{}...` and you need to convert this type into another type. Could you please provide initial type along with the algorithm which should be applied. I mean, it is hard to understand what you need to do even with Animal example. Maybe it is hard only for me because english is not my first (and even second) second. Seems that your problem is pretty interesting. – captain-yossarian from Ukraine Aug 12 '21 at 12:55
  • AFAIK if you want to do such kind of validation you should know up front all possible keys. For example, pls take a look on this article https://dev.to/captainyossarian/how-to-write-a-bit-safer-types-in-typescript-49ge . Find `Part 3`. You may need to create a union of all allowed cases. Otherwise, you need to create a function and infer the argument. It will be much easier to do with function. But this is only my guess – captain-yossarian from Ukraine Aug 12 '21 at 12:58
  • I do know all the keys ahead of time, but because it can be an array of keys, it’s impossible to know the combination of all possibilities even though all the keys are known. That’s the difficulty. So trying to figure out if it can work with array of known possible values, of any combination of those values. Unfortunately I am going to be unavailable for a few days, but I’ll work on an example with more explanation as soon as I can. – JD Isaacks Aug 12 '21 at 14:06
  • when I do ‘type Test = FinalProps<“l.m”>’ I am basically saying give me a type that can optionally have any props from the T[“l”][“m”] type signature. This is solved. I want to also be able to inject another signature into this type by declaring the $inherit prop. so if i create a Test object that has an $inherit: “d.foo” it means, this object can also have a $never prop inherited from T[“d”][“foo”]. This also works but only allowed 1 inherit.. so I want to be able to specify an array to inherit. If that doesn’t make sense, I’ll explain more when I can, I think it’s a straight forward need. – JD Isaacks Aug 12 '21 at 14:15
  • I fully understood first part. Please take a look on my answer and blog here https://stackoverflow.com/questions/67242871/declare-a-type-that-allows-all-parts-of-all-levels-of-another-type#answer-67247652 . Regarding the second part, it is much clearer for me. – captain-yossarian from Ukraine Aug 12 '21 at 15:26
  • 1
    @captain-yossarian I updated the playground link, it now has many more comments explaining the goals and how each part is expected to work. If that is not enough detail we may need to move to chat or something. Also this bounty expires in 3 days, if that matters to you. – JD Isaacks Aug 16 '21 at 12:22
  • thanks. I will take a look. Your question is good, I'm definitely interested )) – captain-yossarian from Ukraine Aug 16 '21 at 12:37
  • You have written: `Each branch has either a `css` or `inherit` prop`. But it might have both of them, right? – captain-yossarian from Ukraine Aug 16 '21 at 12:50
  • 1
    yes it might have both, but has to have at least one. – JD Isaacks Aug 16 '21 at 13:31

1 Answers1

2

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" />
  )
}
  • So the final props is going to go into a StyledComponent as a type argument defining what props can be set on the returned React component. Something like `StyledComponent<"div", any, FinalProps>` In the example K would be "l.m" but could be any branch path. The result is a component that can take those props. So not sure if there is a way for me to pass the generic param when setting the $inherit prop on the component. I would need something like $inherit={["f.w"]} $never="ok" /> but not sure that is possible. So maybe correct answer is, I just can't do this with arrays. – JD Isaacks Aug 17 '21 at 14:44
  • Actually, according to this: https://mariusschulz.com/blog/passing-generics-to-jsx-elements-in-typescript I CAN pass the generic argument to my component, just like I proposed. So, maybe this CAN work then. I will have to play with it. Either way, if it works or not, I'll give you the bounty for your trouble (unless a better answer comes along) – JD Isaacks Aug 17 '21 at 14:50
  • 1
    Could you please provide a reproducable example of react component? TS is able to infer literal [f.w] without using explicit generic. – captain-yossarian from Ukraine Aug 17 '21 at 14:50
  • 1
    Sure, generics for react components are supported since v 3.* – captain-yossarian from Ukraine Aug 17 '21 at 14:51
  • 1
    I will try to come up with a working example, but afraid I won't have time before bounty expires. If so, I'll still give you the bounty and maybe ask that as a new question since applying this to react is not in scope of this question anyways. – JD Isaacks Aug 17 '21 at 14:52
  • 1
    Take a look on my blog https://catchts.com/infer-arguments and https://dev.to/captainyossarian/how-to-type-react-props-as-a-pro-2df2 maybe it will be helpfull. – captain-yossarian from Ukraine Aug 17 '21 at 14:55
  • Sorry, I will not be able to answer on further questions because I'm on vacation now – captain-yossarian from Ukraine Aug 18 '21 at 07:55
  • Let us [continue this discussion in chat](https://chat.stackoverflow.com/rooms/236139/discussion-between-jd-isaacks-and-captain-yossarian). – JD Isaacks Aug 18 '21 at 13:32
  • Ahh ok, enjoy your vacation! I gave you the bounty :). I did post a question in chat, if you can answer after your vacation, if not no worries. – JD Isaacks Aug 18 '21 at 13:46
  • Thanks a lot. I will take a look – captain-yossarian from Ukraine Aug 18 '21 at 14:09
  • @JDIsaacks is this still relevant? I mean the issue with react components and generics? – captain-yossarian from Ukraine Aug 26 '21 at 08:05
  • 1
    Not really. I had it working (kinda) but as my structure grew in complexity I kept hitting the "type is excessively deep and possibly infinite" issue over and over. So I just decided to go with a simpler pattern. I learned a lot about TS in the process tho. – JD Isaacks Aug 27 '21 at 09:54
  • @JDIsaacks I'd like to write an article about this case. Could you please provide some minimum reproducable examples of your react components and use case of this type? my email: sergiybiluk@gmail.com – captain-yossarian from Ukraine Aug 27 '21 at 09:59
  • @JDIsaacks Am I allowed to use react component examples which you have shared in chat in my article? – captain-yossarian from Ukraine Aug 28 '21 at 11:01
  • 1
    Yes, sure. Sorry for the late reply. Go for it :). – JD Isaacks Aug 31 '21 at 16:59