【问题标题】:typescript - how to create type that is build based on arguments parametrizationtypescript - 如何创建基于参数参数化构建的类型
【发布时间】:2021-09-07 18:38:04
【问题描述】:

我正在尝试为我的项目创建一些可扩展的查询解析器。 它应该解析传入的查询字符串并返回类型化的对象。它还应该获取类型化的对象并返回字符串。

让我们想象一下,对于每个单独的参数,我都有我的参数化处理程序 QueryParamHandler

class QueryParamHandler<T> {
    parse(v: string): T;
    stringify(v: T): string;
}

然后我为我想要的每种类型设置了一组处理程序

const stringParser: QueryParamHandler<string> = ...;
const numberParser: QueryParamHandler<number> = ...;
const booleanParser: QueryParamHandler<boolean> = ...;
const dateParser: QueryParamHandler<Date> = ...;

现在我想创建一个包装器,它可以根据我在构造函数中提供的解析器集来解析整套参数

class MyCoolHandler<...> {
    constructor<TP>(handlers: TP extends Record<string, QueryParamHandler<any>>) {}
    parse(query: string): TV? {}
    stringify(vals: TV?): string {}
}

那么我应该如何描述类型 TV(其中所有值都可以未定义)以强制打字稿根据传递的处理程序检查它? 我将在以下示例中描述所需的行为:

const handler = new MyCoolHandler({ str: stringParser, from: dateParser });
handler.stringify({});             // ok
handler.stringify({ str: 'qqq' }); // ok
handler.stringify({ numb: 3 });    // TS error, `numb` key is not allowed here
handler.stringify({ from: 'qq' }); // TS error, `from` key should be Date type

const params = handler.parse('str=&from=2021-09-07')
console.log(params.str)   // ''
console.log(params.numb)  // TS error, params doesn't have 'numb' key

【问题讨论】:

  • 请分享可重现的示例。你从哪里得到TPTV。请摆脱语法错误
  • TP 是我们在构造函数中得到的。电视是我要从最后一个代码示例中获得结果的东西

标签: typescript


【解决方案1】:

您可以使用映射类型为您的 handlers 参数创建适当的类型:

constructor(handlers: {
    [key in keyof TP]: QueryParamHandler<TP[key]>;
}) {}

此类型描述了一个对象,它具有TP 所具有的所有属性,但将这些属性中的值类型重新定义为QueryParamHandler&lt;TP[key]&gt;(其中key 是被迭代的当前属性名称)。现在如果你这样做:

interface SomeObject {
    prop1: string;
    prop2: number;
}

const someHandler = new MyCoolHandler<SomeObject>({});

编译器会抱怨构造函数中缺少prop1prop2,并且您的IDE 将为您提供正确的自动完成功能。

现在你可以像这样输入你的类方法:

class MyCoolHandler<TP extends object> {
    constructor(handlers: {
        [key in keyof TP]: QueryParamHandler<TP[key]>;
    }) {}

    parse(query: string): TP;
    stringify(obj: TP): string;
}

现在someHandler.stringify 将只接受SomeObject 类型的对象。

这是complete example

【讨论】:

  • 谢谢,我也实现了这个非常相似 唯一的问题是我们必须手动设置 TP 我认为 TS 可以以某种方式从“处理程序”类型中获取它,所以你只需要 @987654333 @ 你会得到 someHandler.parse('') =&gt; { str?: string} 的输出类型,因为 stringParser 给了我们字符串
  • 有可能;我刚刚意识到我的游乐场链接完全损坏了,所以我删除了一个没有明确&lt;SomeObject&gt; 规范的更新——它仍然可以正常工作
  • 是的,但在这种情况下,TP 中的所有键都是强制性的,但为了使它们成为可选,我们可以从解析中返回 Partial 谢谢!
【解决方案2】:

让我们为 class 应用一些约束。

class MyCoolHandler<
  ParserType,
  Parser extends QueryParamHandler<ParserType>,
  T extends Record<PropertyKey, Parser>
  > {
  constructor(handlers: T) { }
  parse(query: string) { }
  // stringify will be implemented in a moment
}

const stringParser = new QueryParamHandler<string>()
const dateParser = new QueryParamHandler<Date>()


const handler = new MyCoolHandler({ str: stringParser, from: dateParser });

如果您将鼠标悬停在handler 上,您将看到 TS 已正确推断出所有解析器。

现在我们可以实现stringify 方法了。

我假设stringify 应该接受str 解析器或from 解析器,而不是两者。 让我们实现Either 实用程序:

type QueryParam<T extends QueryParamHandler<any>> =
  T extends QueryParamHandler<infer Param> ? Param : never
{
  // string
  type Test = QueryParam<QueryParamHandler<string>>
}

type Values<T> = T[keyof T]
{
  // 42 | 43
  type Test = Values<{ a: 42, b: 43 }>
}

type Either<T extends Record<string, any>> =
  Values<{
    [Prop in keyof T]: Record<Prop, QueryParam<T[Prop]>>
  }>
{
  // Record<"str", string> | Record<"from", Date>
  type Test = Either<{
    str: QueryParamHandler<string>;
    from: QueryParamHandler<Date>;
  }>
}

现在,一切就绪后,让我们看看我们有什么:

class QueryParamHandler<T> {
  parse(v: T): T;
  stringify(v: T): string;
}


type QueryParam<T extends QueryParamHandler<any>> =
  T extends QueryParamHandler<infer Param> ? Param : never
{
  // string
  type Test = QueryParam<QueryParamHandler<string>>
}

type Values<T> = T[keyof T]
{
  // 42 | 43
  type Test = Values<{ a: 42, b: 43 }>
}

type Either<T extends Record<string, any>> =
  Values<{
    [Prop in keyof T]: Record<Prop, QueryParam<T[Prop]>>
  }>
{
  // Record<"str", string> | Record<"from", Date>
  type Test = Either<{
    str: QueryParamHandler<string>;
    from: QueryParamHandler<Date>;
  }>
}

class MyCoolHandler<
  ParserType,
  Parser extends QueryParamHandler<ParserType>,
  T extends Record<PropertyKey, Parser>
  > {
  constructor(handlers: T) { }
  parse(query: string):T { return null as any }
  stringify(vals: Partial<Either<T>>):string { return null as any }
}

const stringParser = new QueryParamHandler<string>()
const dateParser = new QueryParamHandler<Date>()


const handler = new MyCoolHandler({ str: stringParser, from: dateParser });
handler.stringify({});             // ok
handler.stringify({ str: 'qqq' }); // ok
handler.stringify({ from: new Date() }); // ok

// {
//     str: QueryParamHandler<string>;
//     from: QueryParamHandler<Date>;
// }
const obj = handler.parse('sdf')

handler.stringify({ numb: 3 });    // TS error, `numb` key is not allowed here
handler.stringify({ from: 'qq' }); // TS error, `from` key should be Date type

Playground

至于parse 方法。我不确定你期望什么。 你想像这里一样验证parse 参数吗:

  parse<T extends string>(query: T extends `str${string}from${string}` ? T : never) { }

并从模板文字类型推断出具有适当键和值的对象?

【讨论】:

  • 不,你弄错了 stringify 获取具有应该被字符串化的值的对象。每个值都由在构造函数中为此值传递的解析器进行字符串化。 ('parse' 是反向操作)const h = new MyCoolHandler({ str: stringParser, from: dateParser }); h.stringify({ str: 'qqq', from: new Date() }); // ok h.parse('.') // ok =&gt; { str?: string; from?: Date } 我想对'stringify' 参数和'parse' 输出进行严格的分型检查前面的答案。它正在做我想做的事,但我必须设置值类型。 (可以接受,但我很好奇能否摆脱它)
  • 但在您的示例中,任何一种类型都取决于特定的处理程序,这意味着我将无法将 MyCoolHandler 用于任何参数集
  • 请分享一个例子
猜你喜欢
  • 2011-11-25
  • 2020-09-19
  • 1970-01-01
  • 2018-08-28
  • 2015-08-15
  • 1970-01-01
  • 2017-05-12
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多