英文:
Typescript generic function where parameters compose into the return type
问题
我有一组带有如下签名的类型保护函数:
type Validator<T> = (doc: any) => doc is T;
我想要能够组合这些验证器。例如:
export const union = <T>(validators: Validator<T>[]): ValidatorFn<T> => {
return (doc: any): doc is T => {
for (const validator of validators) {
if (validator(doc)) return true;
}
return false;
}
}
export const intersection = <T>(validators: Validator<T>[]): ValidatorFn<T> => {
return (doc: any): doc is T => {
for (const validator of validators) {
if (!validator(doc)) return false;
}
return true;
}
}
然而,我不太清楚如何为validators
参数设置类型,以便其中的内容要么是T
的一部分,要么与T
相交。例如,以下情况应该正常工作:
interface FooOne {
a: string;
}
interface FooTwo {
b: string;
}
interface FooThree {
c: string
}
type Bar = FooOne | FooTwo
type Baz = FooOne & FooTwo
const oneValidator = validator<FooOne>()
const twoValidator = validator<FooTwo>()
const threeValidator = validator<FooThree>()
const barValidator = union<Bar>([oneValidator, twoValidator]) // 应该成功
const barValidator = union<Bar>([oneValidator]) // 应该成功,因为FooOne足以验证Bar
const barValidator = union<Bar>([oneValidator, twoValidator, threeValidator]) // 应该失败,因为FooThree不包括在Bar中
const bazValidator = intersection<Baz>([oneValidator, twoValidator]) // 应该成功
const bazValidator = intersection<Baz>([oneValidator]) // 应该失败,因为验证FooOne不足以验证Baz
const bazValidator = intersection<Baz>([oneValidator, twoValidator, threeValidator]) // 应该失败,因为FooThree不包括在Baz中
我该如何设置这些类型,以便TypeScript编译器足够聪明以评估这些组合?
英文:
I have a set of type guard functions with the signature:
type Validator<T> = (doc: any) => doc is T;
I want to be able to compose these validators. For example:
export const union = <T>(validators: Validator<>[]): ValidatorFn<T> => {
return (doc: any): doc is T => {
for (const validator of validators) {
if (validator(doc)) return true;
}
return false;
}
}
export const intersection = <T>(validators: Validator<>[]): ValidatorFn<T> => {
return (doc: any): doc is T => {
for (const validator of validators) {
if (!validator(doc)) return false;
}
return true;
}
}
However, i don't really know how to type the validators
param such that whatever is in there is either 'in' T or 'sums up' to T. For example, the following should hopefully work:
interface FooOne {
a: string;
}
interface FooTwo {
b: string;
}
interface FooThree {
c: string
}
type Bar = FooOne | FooTwo
type Baz = FooOne & FooTwo
const oneValidator = validator<FooOne>()
const twoValidator = validator<FooTwo>()
const threeValidator = validator<FooThree>()
const barValidator = union<Bar>([oneValidator, twoValidator]) // should succeed
const barValidator = union<Bar>([oneValidator]) // should succeed because FooOne is sufficient to validate Bar
const barValidator = union<Bar>([oneValidator, twoValidator, threeValidator]) // should fail because FooThree is not in Bar
const bazValidator = intersection<Baz>([oneValidator, twoValidator]) // should succeed
const bazValidator = intersection<Baz>([oneValidator]) // should fail because validating FooOne is insufficient to validate Baz
const bazValidator = intersection<Baz>([oneValidator, twoValidator, threeValidator]) // should fail because FooThree is not in Baz
How can I set up the types so that the typescript compiler is smart enough to evaluate these compositions?
答案1
得分: 2
下面是您要翻译的内容:
- 对于
union
,问题是:“给定一个元组类型T
,如何编写一个类型,它是其所有元素类型的联合类型?” 这相对容易编写;我们只需要使用number
来索引类型,因为元组类型已经具有包含所有元素类型的number
索引签名。(希望这有意义;如果您有一个类型为[string, number, boolean]
的值a
,并使用不越界的number
i
来索引它,那么a[i]
的类型将是string | number | boolean
):
const union = <T extends any[]>(
validators: [...{ [I in keyof T]: Validator<T[I]> }]
): Validator<T[number]> => {
return (doc: any): doc is T[number] => {
for (const validator of validators) {
if (validator(doc)) return true;
}
return false;
}
}
- 对于
intersection
,问题是:“给定一个元组类型T
,如何编写一个类型,它是其所有元素类型的交叉类型?” 这更为复杂。没有简单的方法可以实现这一点...索引访问类型对应于读取属性时的情况,而不是写入属性时的情况。
为了将元组转换为其所有元素类型的交叉类型,我们需要编写自己的实用程序类型,将元组映射到一个版本,其中元素位于逆变类型位置(参见https://stackoverflow.com/q/66410115/2887218),然后使用通过infer
进行条件类型推断来推断这些类型的单一类型,它将成为交叉类型,如文档所述。
它看起来像这样:
type TupleToIntersection<T extends any[]> = {
[I in keyof T]: (x: T[I]) => void
}[number] extends (x: infer R) => void ? R : never;
您可以验证它按预期工作:
type Test = TupleToIntersection<[{ a: string }, { b: number }, { c: boolean }]>;
// type Test = { a: string; } & { b: number; } & { c: boolean; }
因此,intersection
如下:
const intersection = <T extends any[]>(
validators: [...{ [I in keyof T]: Validator<T[I]> }]
): Validator<TupleToIntersection<T>> => {
return ((doc: any): doc is TupleToIntersection<T> => {
for (const validator of validators) {
if (!validator(doc)) return false;
}
return true;
});
}
希望这对您有所帮助。
英文:
My inclination here would be to make union
and intersection
generic in the tuple type T
corresponding to the type each validators
is guarding. For example, if you have validators vx
of type Validator<X>
, vy
of type Validator<Y>
, and vz
of type Validator<Z>
, then when you call union([vx, vy, vz])
we want T
to be [X, Y, Z]
.
Then we can represent the validators
input as a straightforward mapped type on the tuple, like {[I in keyof T]: Validator<T[I]>}
. And in order to give a hint that we want T
to be a tuple and not an unordered array type, we can write it as a variadic tuple type, [...{ I in keyof T]: Validator<T[I]> }]
. Because the mapped type is homomorphic (see https://stackoverflow.com/q/59790508/2887218 ), the compiler is able to infer T
from a value of that mapped type.
That means all we need to worry about is the return types of union
and intersection
.
For union
, the question is: "given a tuple type T
, how do we write a type which is the union of all its element types"? That is relatively easy to write; all we need to do is index into the type with number
, since tuple types already have a number
index signature with the union of element types. (Hopefully that makes sense; if you have a value a
of type [string, number, boolean]
and index into it with a number
i
that isn't out of bounds, then the type of a[i]
will be string | number | boolean
):
const union = <T extends any[]>(
validators: [...{ [I in keyof T]: Validator<T[I]> }]
): Validator<T[number]> => {
return (doc: any): doc is T[number] => {
for (const validator of validators) {
if (validator(doc)) return true;
}
return false;
}
}
And let's test it:
declare const oneValidator: Validator<FooOne>;
declare const twoValidator: Validator<FooTwo>;
declare const threeValidator: Validator<FooThree>;
const uv1 = union([oneValidator]);
// const uv1: Validator<FooOne>
const uv12 = union([oneValidator, twoValidator]);
// const uv12: Validator<FooOne | FooTwo>
const uv123 = union([oneValidator, twoValidator, threeValidator]);
// const uv123: Validator<FooOne | FooTwo | FooThree>
Looks good.
For intersection
, the question is: "given a tuple type T
, how do we write a type which is the intersection of all its element types"? That is more involved. There is no simple way to get this... the indexed access types correspond to what you get when you read properties, not write them.
In order to turn a tuple into the intersection of all its element types, we need to write our own utility type that maps the tuple to a version with the elements in a contravariant type position (see https://stackoverflow.com/q/66410115/2887218) and then use conditional type inference via infer
to infer a single type for those, which will become the intersection, as documented.
It looks like this:
type TupleToIntersection<T extends any[]> = {
[I in keyof T]: (x: T[I]) => void
}[number] extends (x: infer R) => void ? R : never;
which you can verify works as intended:
type Test = TupleToIntersection<[{ a: string }, { b: number }, { c: boolean }]>
// type Test = { a: string; } & { b: number; } & { c: boolean; }
And thus intersection
looks like
const intersection = <T extends any[]>(
validators: [...{ [I in keyof T]: Validator<T[I]> }]
): Validator<TupleToIntersection<T>> => {
return ((doc: any): doc is TupleToIntersection<T> => {
for (const validator of validators) {
if (!validator(doc)) return false;
}
return true;
});
}
And let's test it:
const iv1 = intersection([oneValidator]);
// const iv1: Validator<FooOne>
const iv12 = intersection([oneValidator, twoValidator]);
// const iv12: Validator<FooOne & FooTwo>
const iv123 = intersection([oneValidator, twoValidator, threeValidator]);
// const iv123: Validator<FooOne & FooTwo & FooThree>
Also looks good.
通过集体智慧和协作来改善编程学习和解决问题的方式。致力于成为全球开发者共同参与的知识库,让每个人都能够通过互相帮助和分享经验来进步。
评论