Reputation: 10360
I have a class that accepts a generic
interface Converter<T = Buffer> {
uuid: string;
decode?: (value: Buffer) => T;
encode?: (value: T) => Buffer;
}
type Converters = Record<string, Converter<any>>;
export default class Service<C extends Converters> {
private converters?: Converters;
constructor(converters?: C) {
this.converters = converters;
}
}
The idea is that using the constructor, any type of Converter<T>
can be used with the Service
class. So I could do this:
// Converters using Converter<string>
const converters = {
manufacturer: {
uuid: "2a29",
decode: (buffer: Buffer) => buffer.toString()
},
}
const service = new Service(converters);
this
// Converters using Converter<number>
const converters = {
model: {
uuid: "2a24",
decode: (buffer: Buffer) => buffer.readInt8(0)
}
}
const service = new Service(converters);
this
// Converters using Converter<string> and Converter<number>
const converters = {
manufacturer: {
uuid: "2a29",
decode: (buffer: Buffer) => buffer.toString()
},
model: {
uuid: "2a24",
decode: (buffer: Buffer) => buffer.readInt8(0)
}
}
const service = new Service(converters);
or simply this
const service = new Service();
because converters is optional.
Now the issue here is that the Service
class has a method called read
that will accept a parameter called name
and return a value and that the type of name
and the return type should be based on
Service
classService
class at allif converters have been provided
name
parameterif no converters have been passed, read
should accept any string as parameter and return a Buffer
So if I pass
const converters = {
manufacturer: {
uuid: "2a29",
decode: (buffer: Buffer) => buffer.toString()
},
model: {
uuid: "2a24",
decode: (buffer: Buffer) => buffer.readInt8(0)
}
}
const service = new Service(converters);
const serviceNoConverter = new Service();
This should happen
const value = service.read("manufacturer");
// ^^^^^ type string
const value = service.read("model");
// ^^^^^ type number
const value = service.read("aassd");
// ^^^^^ type error, not manufacturer or model
const value = serviceNoConverter.read("asdasdasd")
// ^^^^^ type Buffer ^^^^^^^^^ any string is allowed
So with the help from StackOverflow (especially from jcalz, thanks buddy) I got so far as to do this:
👇 👇 👇
everything here works exactly as described, but, you can see that at
constructor(converters?: C) {
this.converters = converters;
}
there is an error where converters
does not match this.converters
because converters
is not type-guarded while this.converters
is of the type Converters
. But I can't type-guard the generic as
Service<C extends Converters>
because that will break the functionality of read
as described.
So my question is how can I type-guard the parameter while still keeping this functionality and making it optional?
Upvotes: 0
Views: 105
Reputation: 1
The initial problems in the playground were:
- Overcomplication.
- converters
property type.
As lukasgeiter noted, converters
should be of type C.
However, the default generic value of undefined
is not necessary.
It works perfectly fine like this too:
...
class Service<C extends Converters> {
private converters?: C;
constructor(converters?: C) {
this.converters = converters;
}
public read<N extends keyof C>(name: N): C[N] {
...
}
}
Upvotes: 0
Reputation: 152890
I think constraining C
as Converters | undefined
and undefined
as the default value should work. I would also suggest to change the member variable to be of type C
, now that it is properly constrained:
class Service<C extends Converters | undefined = undefined> {
private converters?: C;
constructor(converters?: C) {
this.converters = converters;
}
//...
}
The key here is that by setting the default to undefined
, creating a service without converters will result in the type: Service<undefined>
instead of Service<Record<...> | undefined>
Upvotes: 2