Typescript通用函数,其中参数组合成返回类型

huangapple go评论62阅读模式
英文:

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&lt;T&gt; = (doc: any) =&gt; doc is T;

I want to be able to compose these validators. For example:

export const union = &lt;T&gt;(validators: Validator&lt;&gt;[]): ValidatorFn&lt;T&gt; =&gt; {
  return (doc: any): doc is T =&gt; {
     for (const validator of validators) {
       if (validator(doc)) return true;
     }
     return false;
  }
}

export const intersection = &lt;T&gt;(validators: Validator&lt;&gt;[]): ValidatorFn&lt;T&gt; =&gt; {
  return (doc: any): doc is T =&gt; {
     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 &amp; FooTwo

const oneValidator = validator&lt;FooOne&gt;()
const twoValidator = validator&lt;FooTwo&gt;()
const threeValidator = validator&lt;FooThree&gt;()


const barValidator = union&lt;Bar&gt;([oneValidator, twoValidator])  // should succeed
const barValidator = union&lt;Bar&gt;([oneValidator])  // should succeed because FooOne is sufficient to validate Bar
const barValidator = union&lt;Bar&gt;([oneValidator, twoValidator, threeValidator])  // should fail because FooThree is not in Bar

const bazValidator = intersection&lt;Baz&gt;([oneValidator, twoValidator]) // should succeed
const bazValidator = intersection&lt;Baz&gt;([oneValidator]) // should fail because validating FooOne is insufficient to validate Baz
const bazValidator = intersection&lt;Baz&gt;([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&lt;X&gt;, vy of type Validator&lt;Y&gt;, and vz of type Validator&lt;Z&gt;, 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&lt;T[I]&gt;}. 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&lt;T[I]&gt; }]. 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 = &lt;T extends any[]&gt;(
    validators: [...{ [I in keyof T]: Validator&lt;T[I]&gt; }]
): Validator&lt;T[number]&gt; =&gt; {
    return (doc: any): doc is T[number] =&gt; {
        for (const validator of validators) {
            if (validator(doc)) return true;
        }
        return false;
    }
}

And let's test it:

declare const oneValidator: Validator&lt;FooOne&gt;;
declare const twoValidator: Validator&lt;FooTwo&gt;;
declare const threeValidator: Validator&lt;FooThree&gt;;

const uv1 = union([oneValidator]);
// const uv1: Validator&lt;FooOne&gt;
const uv12 = union([oneValidator, twoValidator]);
// const uv12: Validator&lt;FooOne | FooTwo&gt;
const uv123 = union([oneValidator, twoValidator, threeValidator]);
// const uv123: Validator&lt;FooOne | FooTwo | FooThree&gt;

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&lt;T extends any[]&gt; = {
    [I in keyof T]: (x: T[I]) =&gt; void
}[number] extends (x: infer R) =&gt; void ? R : never;

which you can verify works as intended:

type Test = TupleToIntersection&lt;[{ a: string }, { b: number }, { c: boolean }]&gt;
// type Test = { a: string; } &amp; { b: number; } &amp; { c: boolean; }

And thus intersection looks like

const intersection = &lt;T extends any[]&gt;(
    validators: [...{ [I in keyof T]: Validator&lt;T[I]&gt; }]
): Validator&lt;TupleToIntersection&lt;T&gt;&gt; =&gt; {
    return ((doc: any): doc is TupleToIntersection&lt;T&gt; =&gt; {
        for (const validator of validators) {
            if (!validator(doc)) return false;
        }
        return true;
    });
}

And let's test it:

const iv1 = intersection([oneValidator]);
// const iv1: Validator&lt;FooOne&gt;
const iv12 = intersection([oneValidator, twoValidator]);
// const iv12: Validator&lt;FooOne &amp; FooTwo&gt;
const iv123 = intersection([oneValidator, twoValidator, threeValidator]);
// const iv123: Validator&lt;FooOne &amp; FooTwo &amp; FooThree&gt;

Also looks good.

Playground link to code

huangapple
  • 本文由 发表于 2023年2月10日 07:28:35
  • 转载请务必保留本文链接:https://go.coder-hub.com/75405517.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定