Lukas
Lukas

Reputation: 10360

Type guarding a generic and defining it as optional

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

if converters have been provided

if 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:

👇 👇 👇

🔗 Link to playground

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

Answers (2)

Alexey Iskhakov
Alexey Iskhakov

Reputation: 1

Here you go.

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

lukasgeiter
lukasgeiter

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>

Playground

Upvotes: 2

Related Questions