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)
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
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.