TLDR; VERSION
Edit, I have added a Playground, Link at the bottom
I have an array ob objects in TypeScript.
interface Converter<T = Buffer> {
name: string;
uuid: CUUID;
decode: (value: Buffer) => T;
}
const infoConverter: Converter<string> = {
name: "info",
uuid: "180a",
decode: v => v.toString()
};
const pressureConverter: Converter<number> = {
name: "pressure",
uuid: "1810",
decode: v => v.readInt32BE(0)
};
type MyConverters = [Converter<string>, Converter<number>]
const converters: MyConverters = [infoConverter, pressureConverter];
Now I am asking if it possible to write a type that is a union of all the names in that array:
type Name<Converters[]> = "???";
// type Name<MyConverters> = "info" | "pressure"
and a type that is a generic corresponding to a name
type Value<Converters[], name extends string>
type Value<MyConverters, "pressure"> = number;
EXPLANATION WHAT I AM DOING AND WHY
I am writing a Bluetooth Low Energy (BLE) Library for Node called Sblendid with which you can talk to BLE peripherals. BLE peripherals have Services from which you can read from and write values to (amongst other things). These sort of "Endpoints" of the Service are called Characteristics and they have IDs (UUIDs).
When you read a Characteristic you need to say which UUID you want to address and the return value will be binary data (usually a Buffer in Node).
So to make life easier for users of the library I want to create an API that lets them provide what I call Converters that look like this:
interface Converter<T = Buffer> {
name: string;
uuid: CUUID;
decode: (value: Buffer) => T;
}
type Converters = Converter<any>[];
So now when somebody wants to talk to a Service on a peripheral they should be able to pass their converters to the Service and can now address their Characteristics by a name and will get their decoded values instead of binary data when the read a Characteristic, like so:
const infoConverter: Converter<string> = {
name: "info",
uuid: "180a",
decode: v => v.toString()
};
const pressureConverter: Converter<number> = {
name: "pressure",
uuid: "1810",
decode: v => v.readInt32BE(0)
};
const converters: Converters = [infoConverter, pressureConverter];
const service = new Service(converters);
const info = await service.read("info");
// ^^^^ ^^^^
// this will be a string their name instead of a UUID (like "a002")
I "already" have a working implementation, but as I am writing everything in TypeScript, I need to get the typings right.
What I want to achieve is that if a user provides the converters as in the example, this:
const service = new Service(converters);
const info = await service.read("infoasdasd");
// ^^^^^^^^^^
should trigger an error saying that this is not a name in your converters array. Also TypeScript should understand that this
const service = new Service(converters);
const info = await service.read("infoasdasd");
// ^^^^
is a string, because of what you wrote in your converters.
Because this all seems easy enough, I want users to be able to work without converters. So if they create a Service without converters
const service = new Service();
const info = service.read("180a");
// ^^^^ ^^^^
// Buffer any CUUID (string | number)
They should be able to pass any Characteristic UUID and will get a Buffer as return value. The implementation works. I am struggling with teaching TypeScript how to type this.
So far I got this:
type CUUID = number | string;
interface Converter<T = Buffer> {
name: string;
uuid: CUUID;
decode: (value: Buffer) => T;
}
type Converters = Converter<any>[];
type NameOrCUUID<C> = C extends Converters ? Name<C> : CUUID;
type Name<C extends Converters> = "info" | "pressure";
type Value<C, N extends NameOrCUUID<C>> = "" | Buffer;
class Service<C> {
private converters?: Converters;
constructor(converters?: C) {
this.converters = converters as any; // Ask on StackOverflow
}
public read<N extends NameOrCUUID<C>>(name: N): Value<C, N> {
if (this.converters) {
return this.getConvertedValue(name);
} else {
return this.getBufferForUUID(name);
}
}
private getBufferForUUID(uuid: CUUID): Buffer {
return Buffer.from("Foobar", "utf8"); // Dummy Code
}
private getConvertedValue<N extends NameOrCUUID<C>>(name: N): Value<C, N> {
return "Foobar"; // Dummy Code
}
}
Of course, this is not the actual implementation for the BLE. With this code I am trying to get the typings right. So what I am trying is to tell read that it can accept a NameOrCUUID, which, depending on if the Service class has a generic that is Converters will either a name of the Converters or if there was no generic passed to the Service class will accept any Characteristic UUID.
This works. When I call this code:
const service = new Service(converters);
const info = service.read("info");
const service2 = new Service();
const info2 = service2.read("180a");
and pass the converters from way up there as a parameter it will tell me that I can only pass info or pressure. I I do not pass any converters at all (service2) I can pass any number or string.
But this is only fake because Implementations for
type Name<C extends Converters> = "???";
type Value<C, N extends NameOrCUUID<C>> = "???";
For this, and this and this is the part I cannot figure out.
I would need to tell TypeScript to allow the name properties of any item in Converters likewise with the values, and the values must match the name.
Ok sorry for the wall of text. I hope I could express what I am asking (that last paragraph is the actual question). I wonder if that is even possible in TypeScript.
Here is a Playground:
MyConvertersis)