typescript 使泛型参数基于另一个泛型参数成为可选参数?

z2acfund  于 2023-03-31  发布在  TypeScript
关注(0)|答案(1)|浏览(165)

我正在尝试创建一个泛型,如下所示:

type HTTPMethod = "HEAD" | "GET" | "POST" | "PUT" | "PATCH" | "DELETE";
type Endpoint<M extends HTTPMethod, S extends {}> = () => void;

“想法”是在声明Endpoint函数时提供形状SS可以用作输入/输出类型。
但是,我希望S对于POST、PUT、PATCH是必需的,但是对于HEAD、GET、DELETE是可选的。类似于这样:

const a: Endpoint = () => {}; // < requires 1 type argument(s).
const b: Endpoint<"HEAD"> = () => {};
const c: Endpoint<"HEAD", {}> = () => {};
const d: Endpoint<"POST"> = () => {}; // < requires 2 type argument(s).
const e: Endpoint<"POST", {}> = () => {};

看起来好像使用extends=并不像我预期的那样工作。

type Test<T extends string = number> = () => void;

错误,因为number不满足string。但是:

type Endpoint<M extends HTTPMethod, S extends M extends "POST" | "PUT" | "PATCH" 
  ? S extends unknown 
  ? never
  : {} 
  : never = unknown> = () => void;
  • 总是有效的,尽管事实上unknown不是never也不是{}。我已经玩了几个小时以上,但我似乎找不到什么工作。
    这样的事情在当前的TypeScript中可能吗?
kjthegm6

kjthegm61#

TypeScript中的Generic类型参数可以有默认的类型参数,但是如果你使用它们,类型参数是可选的,如果你不使用类型参数,类型参数是必需的。在TypeScript中没有“条件可选”的类型参数。这意味着虽然可能获得与你想要的类似的行为,但没有任何功能可以提供它。
一个合理的解决方法是使用单个元组类型的类型参数,而不是可变数量的类型参数。因此,您可以编写Endpoint<X, Y>而不是Endpoint<[X, Y]>。与泛型类型参数相比,对可变长度元组的支持要多得多,并且其长度取决于某些条件。
这里有一个方法可以做到这一点:

type NeedExtra = "POST" | "PUT" | "PATCH";

type Allowed = HTTPMethod extends infer H ? H extends NeedExtra ?
    [H, any] : [H, any?] : never;

// type Allowed = ["HEAD", any?] | ["GET", any?] | ["POST", any] | 
//  ["PUT", any] | ["PATCH", any] | ["DELETE", any?]

所以Allowed是两个元素的元组类型的并集,其中第二个元素是可选的,取决于第一个元素类型。
然后,您可以将Endpoint定义为将其类型参数约束为Allowed

type Endpoint<M extends Allowed> = () => void;
const a: Endpoint<[]> = () => { }; // error
// -------------> ~~
// Source has 0 element(s) but target requires 1
const b: Endpoint<["HEAD"]> = () => { };
const c: Endpoint<["HEAD", {}]> = () => { };
const d: Endpoint<["POST"]> = () => { }; // error
// -------------> ~~~~~~~~
// Type at position 0 in source is not compatible with type at position 0 in target.
const e: Endpoint<["POST", {}]> = () => { };

这就是你想要的,缺点是它需要一个元组 Package 器,但优点是它相当简单,你可以只看Allowed,然后读出你应该做什么和不应该做什么。
另一种解决方法是让所需的错误出现在 first 类型参数上,而不是“丢失”的第二个参数上。也就是说,我们不能让第二个参数“有条件可选”,但我们可以约束第一个类型参数,这样第二个类型参数的不需要的值会使第一个类型参数无法满足其约束。
它可能看起来像这样:

type Endpoint<T extends (
    [T, U] extends [NeedExtra, never] ?
    Exclude<HTTPMethod, NeedExtra> :
    HTTPMethod
), U = never> = () => void

因此,第一个类型参数T被约束为依赖于它自己和第二个类型参数U的某个参数。如果是的话,我们可以把它移到第三个类型参数......但是我离题了)。我假设如果第二个类型参数Unever,那么它就和missing一样(如果你需要区分nevermissing,那么我们可以创建一些“sigil”类型,但是这更奇怪)。
因此,T的约束为HTTPMethodExclude<HTTPMethod, NeedExtra>(不是NeedExtra的所有内容),取决于TU。如果T可分配给NeedExtra,但Unever,那么就有问题了,我们把T约束为所有不是NeedExtra的东西,这会失败,否则就没有问题,我们把T约束为HTTPMethod
我们试试看:

const a: Endpoint = () => { };
// ----> ~~~~~~~~
// error, Generic type 'Endpoint' requires between 1 and 3 type arguments.
const b: Endpoint<"HEAD"> = () => { };
const c: Endpoint<"HEAD", {}> = () => { };
const d: Endpoint<"POST"> = () => { }; // error, 
// -------------> ~~~~~~
// Type 'string' does not satisfy the constraint '"HEAD" | "GET" | "DELETE"'.
const e: Endpoint<"POST", {}> = () => { };

看起来也不错。错误是合理的,并且具有非常接近你想要的优点。缺点是约束更加神秘,并且绝对不是 * 旨在 * 由语言支持。我可以想象未来TypeScript版本的更改将打破这一点并需要重构(特别是循环)。但目前它工作正常,并不可怕。
Playground代码链接

相关问题