将 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;
}
注意:RootValue
和 ArgsValue
是用于获取 "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,所以我仍然愿意接受建议和更好的解决方案。
这与 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;
}
注意:RootValue
和 ArgsValue
是用于获取 "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,所以我仍然愿意接受建议和更好的解决方案。