【问题标题】:TypeScript: Make properties allowed on object depend on values in other property arrayTypeScript:使对象上允许的属性取决于其他属性数组中的值
【发布时间】:2021-10-14 00:21:54
【问题描述】:

假设我有一个带参数的泛型类型:

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

然后说我想根据所述类型的特定属性使用 AnimalProps 更改类型:

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

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

然后我希望能够做到:

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">
}

我能够做到这一点,通过执行以下操作:

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>
>

但我似乎无法弄清楚的部分是将其扩展为各种类型的数组:

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">
}

如果我尝试这样定义我的动物结构:

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[]>

我已经尝试了几件事,但我只能得到 allnothing 的意思,我向我的对象添加了一系列动物种类并且没有动物道具允许或所有动物的所有道具都是允许的(不仅仅是我指定的那些)。

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.
}

这是一个人为的例子,一个可能超出范围的更复杂的问题,但这里是playground 链接到一个更具体的例子。底部显示了一系列我认为是有效类型或无效类型的结构。除了最后一个,其他都通过了。

标签: typescript typescript-generics


【解决方案1】:

首先,在这种情况下,如果没有额外的泛型参数,就不可能做这样的事情:

const f:Test = { 
  $transform: 'a',
  $font: 'a',
  $inherit: ['f.w', 'one.more'],
  $never: 'a',
  $time: 'a',
  $cant: 'a', // compilation error
}

为了推断$inherit: ['f.w', 'one.more'] 所需的道具,您应该为Test 使用函数或额外的通用参数。

类似这样的:

const f:Test<['f.w', 'one.more']> = { 
  $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'])

如果没有额外的参数,您应该计算所有可能的$inherit 属性和对象本身的排列。它将达到递归限制。

这里有一个额外通用的解决方案:

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

请让我知道它是否适合您。如果是这样 - 我会提供一些解释。

OP 提供的 React 使用示例 - @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" />
  )
}

【讨论】:

  • 您能否提供一个可重现的反应组件示例? TS 能够在不使用显式泛型的情况下推断文字 [f.w]。
  • 当然,从 v 3.* 开始支持反应组件的泛型。*
  • 我将尝试提出一个可行的示例,但担心在赏金到期之前我没有时间。如果是这样,我仍然会给你赏金,也许会作为一个新问题提出这个问题,因为无论如何应用它来做出反应都不在这个问题的范围内。
  • 并非如此。我让它工作了(有点),但是随着我的结构变得越来越复杂,我一遍又一遍地遇到“类型过深并且可能无限”的问题。所以我决定采用更简单的模式。在这个过程中我学到了很多关于 TS 的知识。
猜你喜欢
  • 2016-02-23
  • 1970-01-01
  • 1970-01-01
  • 2021-11-23
  • 1970-01-01
  • 1970-01-01
  • 2021-10-17
  • 1970-01-01
相关资源
最近更新 更多