简体   繁体   中英

Default value for optional field in a constructor of a generic class

I'm trying to figure if it's possible (using type declarations alone) to somehow fit a concrete default value for an optional field --- in the context of a generic class, such that the type of this default would be still tied to another required field

    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
        }

    }

Where the default transformer can be something like (just passes the value through):

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

At the moment it (rightfully) warns me that T could be instantiated with a type that has nothing to do with SimpleMsg

I would, therefore - somehow like to define the type of the handler in terms of the (type?) of the transformer (or vice versa?) - and force that type to be SimpleMsg in case the transformer is undefined (ie not provided)

I know it can be worked around using factory methods or something, where I explicitly define the handler as Handler<SimpleMsg> , but I really want to know if it's solvable with types and a single entry point

Thanks!

A Transformer returns T so we cannot possibly create a default without knowing what T is. But you've got the right idea here:

and force that type to be SimpleMsg in case the transformer is undefined (ie not provided)

Constructor Overloading

We can do this by overloading the constructor function with multiple argument types. We allow any matching pair of handler and transformer functions for the same T OR just a handler which takes a SimpleMsg . Typescript cannot infer the type of T in the second case and will return Consumer<unknown> , so we have to set the default value of T for the class to SimpleMsg .

The body of the constructor function only knows the types from the implementation signature, which is the same signature that you had before. So we do need to assert that the defaultTransformer is the right type.

(I renamed Transformer to MsgTransformer to avoid a duplicate type error that I was getting)

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>;
    }

}

Test cases:

// 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

Edit: Typing an Options Object

In the case where the handler and transformer are two properties on the same object we would not use overloads. We would just create a more complicated type for the arguments.

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

We can use a conditional type which makes transformer optional only if the SimpleMsg returned for the defaultTransformer is assignable to T .

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

Or we could use a union type. This is less type-safe because you could manually declare T as any arbitrary type when passing a Handler<SimpleMsg> ( new Consumer<SomeWrongType>(options) ). But that seems unlikely to be an issue.

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

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM