将 Typescript 泛型与 any 结合起来而不丢失类型

Combining Typescript generics with any without losing type

这与 plugin I am building for the @nexus/schema 库(类型安全的 GraphQL)有关,但这纯粹是一个 Typescript 类型问题。

我有一个规则系统,我的所有规则都来自这个界面:

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

注意:RootValueArgsValue 是用于获取 "real" 生成类型或 return 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)
}

我的问题是我需要能够支持适用于所有 types/fields 的通用规则和仅适用于一个 type/field 的特定规则。但是,如果我将通用规则与特定规则结合使用,则生成的规则是 Rule<any, any>,然后允许接受错误规则。

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,但是有没有我可以用来防止的解决方法any 类型覆盖了我的类型。我发现的一种解决方法是显式键入 and,但从可用性的角度来看这并不好。

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

编辑 3: 正如评论中指出的那样,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 return 是正确输入的 FullRule<'MyType','MyField'>,因此会拒绝 badRule。但它确实需要我添加新方法来创建部分和通用规则。

感谢@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;

有了这些,返回的类型就变成了泛型函数。当您调用 "typed context" 中的函数时,它将阻止 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,所以我仍然愿意接受建议和更好的解决方案。