【问题标题】:Combining Typescript generics with any without losing type将 Typescript 泛型与 any 相结合而不会丢失类型
【发布时间】:2020-10-03 09:08:21
【问题描述】:

这与我正在为 @nexus/schema 库(类型安全的 GraphQL)构建的 plugin 有关,但这纯粹是 Typescript 键入问题。

我有一个规则系统,我的所有规则都是从这个接口派生的:

interface Rule<Type extends string, Field extends string> {
  resolve(root: RootValue<Type>, args: ArgsValue<Type, Field>): boolean;
}

注意:RootValueArgsValue 是用于获取“真实”生成类型或返回any 的类型,这是nexus 用于键入所有内容的技巧,无需显式指定类型。 See this link for the source code.

最基本的两个是:

type Options = { cache?: boolean }

type RuleFunc<Type extends string, Field extends string> =
  (root: RootValue<Type>, args: ArgsValue<Type, Field>) => boolean;

class BaseRule<Type extends string, Field extends string> implements Rule<Type, Field> {
  constructor(private options: Options, private func: RuleFunc<Type, Field>) {}

  resolve(root: RootValue<Type>, args: ArgsValue<Type, Field>) {
    // Do stuff with the options
    const result = this.func(root, args)
    return result
  }
}

class AndRule<Type extends string, Field extends string> implements Rule<Type, Field> {
  constructor(private rules: Rule<Type, Field>[]) { }

  resolve(root: RootValue<Type>, args: ArgsValue<Type, Field>) {
    return this.rules
      .map(r => r.resolve(root, args))
      .reduce((acc, val) => acc && val)
  }
}

然后我定义助手:

const rule = (options?: Options) =>
  <Type extends string, Field extends string>(func: RuleFunc<Type, Field>): Rule<Type, Field> => {
    options = options || {};
    return new BaseRule<Type, Field>(options, func);
  };

const and = <Type extends string, Field extends string>(...rules: Rule<Type, Field>[]): Rule<Type, Field> => {
  return new AndRule(rules)
}

我的问题是我需要能够支持适用于所有类型/字段的通用规则和仅适用于一种类型/字段的特定规则。但是,如果我将通用规则与特定规则结合起来,则生成的规则是 Rule&lt;any, any&gt;,然后允许接受错误的规则。

const genericRule = rule()<any, any>((root, args) => { return true; })

const myBadRule = rule()<"OtherType", "OtherField">((root, args) => {
  return true;
})

const myRule: Rule<"Test", "prop"> = and(
  rule()((root, args) => {
    return false
  }),
  genericRule,
  myBadRule // THIS SHOULD BE AN ERROR
)

我猜这部分是因为 Typescript 中缺乏存在类型,这基本上迫使我首先使用 any,但是有没有一种解决方法可以用来防止 @ 的类型987654338@ 覆盖我的类型。我发现的一种解决方法是显式键入and,但从可用性的角度来看这并不好。

编辑 2:我创建了一个 playground with a simplified version so it easier to view the problem

编辑 3: 正如 cmets 中所指出的,never 与前面的示例一起使用。 I thus created this example for which never does not work。我还修改了这个问题,以便所有信息都在问题中以供后代使用。我也发现never不能用的原因是ArgsValue类型。

非常感谢!

编辑 1:

我找到了一种解决方法,尽管它需要更改界面:

export interface FullRule<
  Type extends string,
  Field extends string
> {
  resolve(
    root: RootValue<Type>,
    args: ArgsValue<Type, Field>,
  ): boolean;
}

export interface PartialRule<Type extends string>
  extends FullRule<Type, any> {}

export interface GenericRule extends FullRule<any, any> {}

export type Rule<Type extends string, Field extends string> =
  | FullRule<TypeName, FieldName>
  | PartialRule<TypeName>
  | GenericRule;

随着and 变成:

export const and = <Type extends string, Field extends string>(
  ...rules: Rule<Type, Field>[]
): FullRule<Type, Field> => {
  return new RuleAnd<Type, Field>(rules);
};

and 返回一个正确键入的FullRule&lt;'MyType','MyField'&gt;,因此将拒绝badRule。但它确实需要我添加新方法来创建部分规则和通用规则。

【问题讨论】:

  • 请考虑将示例代码转换为minimal reproducible example,可以将其放入The TypeScript Playground 等独立IDE 中,以展示您所看到的内容。或者,可以添加一个链接到已经处理了库依赖项的现有 Web IDE?
  • 老实说,我真的尝试过在codesanbox中显示有点困难,最好是在这里运行我的示例:github.com/Sytten/nexus-shield/tree/master/examples/simple
  • 我不知道你能指望别人在他们的本地系统上安装项目;我个人不太可能这样做。
  • @jcalz 我创建了一个带有简化版问题的游乐场(请参阅帖子中的编辑)。很抱歉在我之前的回复中是个混蛋。如果您能看一看,我将不胜感激!
  • Blecch,那些关系助手需要大量的条件输入。我看到的问题是编译器不接受Rule&lt;A, B&gt;AB 中是逆变的,尽管我认为它应该是。大多数“不错”的解决方案将涉及使用Rule&lt;string, string&gt; 作为通用规则,而and() 自然只会接受正确的类型。但这并没有发生,可能是因为那些助手中的东西。很难看到如何在不扔掉这些东西的情况下修复它。 ??????

标签: typescript strong-typing nexus-prisma


【解决方案1】:

感谢@jcalz,我能够了解更多关于 Typescript 的打字系统的东西,并且基本上意识到即使我能够使用 nexus 的复杂助手进行逆变,它也无法实现我想要的做。

所以我采取了另一种方法。它并不完美,但效果很好。我定义了两个新的运算符:

export const generic = (rule: Rule<any, any>) => <
  Type extends string,
  Field extends string
>(): Rule<Type, Field> => rule;

export const partial = <Type extends string>(rule: Rule<Type, any>) => <
  T extends Type, // NOTE: It would be best to do something with this type
  Field extends string
>(): Rule<Type, Field> => rule;

有了这些,返回的类型就变成了一个泛型函数。当您在“类型化上下文”中调用该函数时,它会阻止any 的传播。

const genericRule = generic(rule()((root, args) => { return true; }))

const myBadRule = rule()<"OtherType", "OtherField">((root, args) => {
  return true;
})

const myRule: Rule<"Test", "prop"> = and(
  rule()((root, args) => {
    return false
  }),
  genericRule(), //  Returns a Rule<"Test", "prop">
  myBadRule // ERROR
)

从某种意义上说,混合部分规则和通用规则非常冗长,并且需要在父帮助程序中指定类型,这并不完美:

const myPartialType = partial<'Test'>(
  rule()((root, _args, ctx) => {
    return true;
  })
);

const myCombination = partial<'Test'>(
  chain(
    isAuthenticated(),
    myPartialType()
  )
);

我仍然觉得这有点像 hack,所以我仍然愿意接受建议和更好的解决方案。

【讨论】:

    猜你喜欢
    • 2020-04-07
    • 1970-01-01
    • 2021-07-23
    • 2019-05-26
    • 2017-06-02
    • 2019-08-01
    • 2021-07-13
    • 1970-01-01
    相关资源
    最近更新 更多