TLDR;版本
编辑,我在底部添加了一个Playground,链接
我在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];
现在我在问是否可以编写一种类型,该类型是该数组中所有name
的并集:
type Name<Converters[]> = "???";
// type Name<MyConverters> = "info" | "pressure"
和与名称相对应的通用类型
type Value<Converters[], name extends string>
type Value<MyConverters, "pressure"> = number;
我正在做什么和为什么进行解释
我正在写一个Bluetooth Low Energy (BLE) Library for Node called Sblendid,您可以用它与BLE外设通信。 BLE外设具有Services
,您可以从中读取值和向其中写入值(除其他外)。服务的这种“端点”称为Characteristics
,它们具有ID(UUID)。
读取特征时,您需要说出要寻址的UUID,返回值将是二进制数据(通常在Node中为Buffer
)。
因此,为了使图书馆用户的工作更轻松,我想创建一个API,让他们提供我称之为Converters
的内容,如下所示:
interface Converter<T = Buffer> {
name: string;
uuid: CUUID;
decode: (value: Buffer) => T;
}
type Converters = Converter<any>[];
因此,现在当有人想与外围设备上的Service
进行对话时,他们应该能够将其转换器传递到Service
,并且现在可以通过名称和名称来寻址他们的Characteristics
读取Characteristic
时,获取其解码值而不是二进制数据,如下所示:
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")
我“已经”有一个可行的实现,但是当我在TypeScript
中编写所有内容时,我需要正确输入文字。
我想要实现的是,如果用户如示例中那样提供converters
,则:
const service = new Service(converters);
const info = await service.read("infoasdasd");
// ^^^^^^^^^^
应该触发错误,说这不是您的converters
数组中的名称。另外TypeScript应该了解这一点
const service = new Service(converters);
const info = await service.read("infoasdasd");
// ^^^^
是一个字符串,因为您在converters
中写了什么。
因为这一切看起来都很简单,所以我希望用户无需使用converters
就可以工作。因此,如果他们创建的Service
不包含converters
const service = new Service();
const info = service.read("180a");
// ^^^^ ^^^^
// Buffer any CUUID (string | number)
它们应该能够传递任何特性UUID,并将获得Buffer作为返回值。实施工作。我正在努力教TypeScript
如何输入此字符。
到目前为止,我知道了:
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
}
}
当然,这不是BLE的实际实现。通过此代码,我试图正确地进行键入。因此,我要尝试告诉read
它可以接受NameOrCUUID
,这取决于Service
类是否具有Converters
泛型,或者{ name
中的{1}},或者如果没有传递给Converters
类的泛型,将接受任何特征性UUID。
这有效。当我调用此代码时:
Service
并从上方传递const service = new Service(converters);
const info = service.read("info");
const service2 = new Service();
const info2 = service2.read("180a");
作为参数,它将告诉我只能传递converters
或info
。我根本不传递任何转换器(pressure
)我可以传递任何数字或字符串。
但这只是假的,因为实现
service2
为此,这是我无法弄清的部分。
我需要告诉type Name<C extends Converters> = "???";
type Value<C, N extends NameOrCUUID<C>> = "???";
允许TypeScript
中任何项目的name
属性都带有值,并且值必须与名称匹配。
对不起,我在此留言。我希望我能表达我的要求(最后一段是实际的问题)。我不知道在TypeScript中是否有可能。
这是一个游乐场:
答案 0 :(得分:1)
我看到的第一个问题是,您将infoConverter
的类型注释(如Converter<string>
和pressureConverter
(如Converter<number>
)太宽了。编译器会尽职尽责地忘记infoConverter.name
是"info"
,而是使用表示它是string
的类型注释。
我认为通过使用类型推断和const
assertion为转换器实例获取最窄的类型,您将更接近想要的东西:
const infoConverter = {
name: "info",
uuid: "180a",
decode: (v: Buffer) => v.toString()
} as const;
const pressureConverter = {
name: "pressure",
uuid: "1810",
decode: (v: Buffer) => v / 1024
} as const;
const converters = [infoConverter, pressureConverter] as const;
目前,不能保证converters
的元素符合您的Converter
接口,但是您可以确保任何接受converters
的函数或接受{{1 }}限制为typeof converters
,如果Converter<any>[]
有错误,您将在此得到错误提示。
因此,让我们看一下您想要的这两个类型函数...给定一组converters
类型的数组,获取Converter
的列表,以及name
到{{ 1}}类型。怎么样:
name
请注意,我将value
约束为type Names<C extends ReadonlyArray<Converter<any>>> = C[number]["name"];
type Value<
C extends ReadonlyArray<Converter<any>>,
N extends Names<C>
> = Extract<C[number], { name: N }> extends Converter<infer V> ? V : never;
,而不是C
...这是一个宽松的约束(ReadonlyArray<Converter<any>>
接口是{{1}的扩展}接口;每个Array<Converter<any>>
都是Array
,但反之则不;名称可能有点误导;对于ReadonlyArray
和Array
,它应该是ReadonlyArray
ReadableAndWritableArray
...,但我离题了。这样Array
将接受readonly tuple
的ReadableArray
。
无论如何,ReadonlyArray
数组的C
索引元素中的typeof converters
类型别名looks up是Names
属性类型。 "name"
类型别名Extract
s的number
元素具有C
属性与Value
匹配的元素,然后infers从中得到的值类型
行得通吗?
C
对我很好。请注意,以上几行没有编译器错误,因此name
对象必须正确满足作为N
元素(可能是只读)数组的约束。
希望您可以使用它来使扩展的示例代码正常工作。祝你好运!