【问题标题】:Default value for optional field in a constructor of a generic class泛型类的构造函数中可选字段的默认值
【发布时间】:2021-05-29 20:34:38
【问题描述】:

我试图弄清楚是否有可能(仅使用类型声明)以某种方式适合可选字段的具体默认值 --- 在泛型类的上下文中,这样这个默认值的类型仍然是绑定到另一个必填字段

    type Handler<T> = (msg: T) => boolean;

    type Transformer<T> = (msg: SimpleMsg) => T;

    class Consumer<T> {

        handler: Handler<T>;
        transformer: Transformer<T>;

        constructor(handler: Handler<T>, transformer?: Transformer<T>) {
            this.handler = handler;
            this.transformer = transformer || defaultTransformer
        }

    }

默认转换器可以是这样的(只是传递值):

    const defaultTransformer = (msg: SimpleMsg) => {
        console.log('BoringTranfomer! ' + JSON.stringify(msg));
        return msg;
    } 

目前它(正确地)警告我 T 可以用与 SimpleMsg 无关的类型实例化

因此,我会 - 以某种方式想以 terms 的变压器(类型?)定义处理程序的类型(反之亦然?) - 并强制该类型为 @987654326 @以防变压器未定义(即未提供)

我知道可以使用工厂方法或其他方法来解决它,我将处理程序明确定义为Handler&lt;SimpleMsg&gt;,但我真的想知道它是否可以通过类型和单个入口点解决

谢谢!

【问题讨论】:

  • 完整的旁注,但它困扰我PlainPojo 是一个坏名字。 POJO 中的 P 已经代表“plain”。所以,PlainPojo 是一个“普通的 JavaScript 对象”
  • 我喜欢 90%。我已经知道了,因此如果handler 采用SimpleMsg,您只能省略transformer,但在那种情况下我无法将T 推断为SimpleMsgtsplay.dev/lWYXbm
  • 没关系,我们可以将SimpleMsg设置为默认值:)

标签: typescript generics typescript-generics conditional-types


【解决方案1】:

Transformer 返回T,所以我们不可能在不知道T 是什么的情况下创建默认值。但是您在这里的想法是正确的:

如果转换器未定义(即未提供),则强制该类型为 SimpleMsg

构造函数重载

我们可以通过使用多个参数类型重载constructor 函数来做到这一点。我们允许任何匹配的 handlertransformer 函数对用于相同的 T 或只是一个 handler,它需要一个 SimpleMsg。 Typescript 在第二种情况下无法推断出T 的类型,会返回Consumer&lt;unknown&gt;,因此我们必须将类的默认值T 设置为SimpleMsg

构造函数的主体只知道实现签名中的类型,这与您之前的签名相同。所以我们确实需要断言defaultTransformer 是正确的类型。

(我将 Transformer 重命名为 MsgTransformer 以避免出现重复的类型错误)

class Consumer<T = SimpleMsg> {

    handler: Handler<T>;
    transformer: MsgTransformer<T>;

    // if a transformer is provided, it must match the handler    
    constructor(handler: Handler<T>, transformer: MsgTransformer<T>)
    // if no transformer is provided, then the handler must be for type SimpleMsg
    constructor(handler: Handler<SimpleMsg>)
    // implementation signature which combines all overloads
    constructor(handler: Handler<T>, transformer?: MsgTransformer<T>) {
        this.handler = handler;
        this.transformer = transformer || defaultTransformer as MsgTransformer<T>;
    }

}

测试用例:

// CAN pass just a handler for a SimpleMsg
const a = new Consumer((msg: SimpleMsg) => true);  // <SimpleMsg>
const b = new Consumer(() => true); // <SimpleMsg>
// CANNOT pass just a handler for another type
const c = new Consumer((msg: { something: string }) => true); // error as expected
// CAN pass a handler and a transformer that match
const d = new Consumer((msg: { something: string }) => true, (msg: SimpleMsg) => ({ something: "" })); // generic <{something: string}>
// CANNOT have mismatch between handler and transformer
const e = new Consumer((msg: { something: string }) => true, (msg: SimpleMsg) => ({})); // error as expected

Typescript Playground Link

编辑:键入选项对象

如果handlertransformer 是同一对象上的两个属性,我们不会使用重载。我们只会为参数创建一个更复杂的类型。

constructor({handler, transformer}: Options<T>) {

只有当为defaultTransformer 返回的SimpleMsg 可分配给T 时,我们才能使用使transformer 成为可选的条件类型。

type Options<T> = SimpleMsg extends T ? {
    handler: Handler<T>;
    transformer?: MsgTransformer<T>; // make optional
} : {
    handler: Handler<T>;
    transformer: MsgTransformer<T>;
}

或者我们可以使用联合类型。这不太安全,因为您可以在传递Handler&lt;SimpleMsg&gt; (new Consumer&lt;SomeWrongType&gt;(options)) 时手动将T 声明为任意类型。但这似乎不太可能成为问题。

type Options<T> = {
    handler: Handler<T>;
    transformer: MsgTransformer<T>;
} | {
    handler: Handler<SimpleMsg>;
    transformer?: never; // need this in order to destructure
}

【讨论】:

  • 谢谢,这很有趣!我为此奋斗了好几个小时,试图只用类型来解决它,没想到要和构造函数一起玩....现在,我简化了这个问题的用例,但是 - 在我的实际用例中,我正在传递包含在 opts: {...} 对象中的所有字段...这将如何影响解决方案?我是否必须开始创建“自定义”类型以匹配每个构造函数中的各种可能输入,使用“省略”之类的?
  • 是的,看来它有效!我在变压器上创建了一个带有“Omit”的构造函数,另一个使用我创建的类型命名为:“RequireOne”,它将可选参数变为必需参数。虽然,这甚至不是必需的,因为如果我只是将它从选项对象中省略,则选项对象中的转换器是可选的这一事实不再相关......
  • 我们不希望它是可选的,除非 T 是默认值。您将使用两种类型的联合 { handler: Handler&lt;T&gt;; transformer: Transformer&lt;T&gt;; } | { handler: &lt;SimpleMsg&gt;; }
  • 为了从联合中解构一个属性,它必须存在于联合中的每个类型上。所以我们说它“存在”但是是可选的,只能是undefined
  • 一个重载的函数并不真正意识到它的重载。它需要仅基于通常更模糊的实现签名来工作。在选项对象的情况下,也许您可​​以编写它以便您不需要断言,但这可能会很棘手,因为缩小变量会缩小变量类型而不是T 类型。在重载的情况下,您将始终需要断言。
猜你喜欢
  • 2011-03-19
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2021-06-10
  • 1970-01-01
相关资源
最近更新 更多