【问题标题】:TypeScript: Union of a specific property of items in arrayTypeScript:数组中项目的特定属性的联合
【发布时间】:2019-09-05 19:42:17
【问题描述】:

TLDR;版本

编辑,我在底部添加了一个 Playground,链接

我在 TypeScript 中有一个数组 ob 对象。

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];

现在我在问是否可以编写一个类型,它是该数组中所有 names 的联合:

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 的情况下工作。因此,如果他们创建一个没有convertersService

const service = new Service();
const info = service.read("180a");
//    ^^^^                 ^^^^
//    Buffer               any CUUID (string | number)

他们应该能够传递任何特征 UUID 并将获得一个缓冲区作为返回值。实施有效。我正在努力教 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的泛型nameConverters或如果没有泛型传递给 Service 类将接受任何特征 UUID。

这行得通。当我调用此代码时:

const service = new Service(converters);
const info = service.read("info");

const service2 = new Service();
const info2 = service2.read("180a");

并从那里传递converters 作为参数,它会告诉我我只能传递infopressure。我根本不传递任何转换器 (service2) 我可以传递任何数字或字符串。

但这只是假的,因为实现

type Name<C extends Converters> = "???";
type Value<C, N extends NameOrCUUID<C>> = "???";

对于这个,这个和这个是我想不通的部分。

我需要告诉TypeScript 允许Converters 中任何项目的name 属性与值一样,并且值必须与名称匹配。

好吧,对不起,文字墙。我希望我能表达我的要求(最后一段是实际问题)。我想知道这在 TypeScript 中是否可行。

这里是游乐场:

This elaborate example on typescriptlang.org/play

【问题讨论】:

  • "tuple" 可能是一个不错的关键字添加到此处(即 MyConverters 是什么)

标签: typescript


【解决方案1】:

我看到的第一个问题是您的 infoConverterConverter&lt;string&gt;pressureConverterConverter&lt;number&gt; 的类型注释太宽了。编译器会尽职尽责地忘记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 的函数或接受typeof converters 的类型都被限制为Converter&lt;any&gt;[]如果converters 有错误,你会在那里得到一个错误。

让我们看看你想要的那两个类型函数...给定一个Converter 类型的数组,获取names 的列表,以及namevalue 类型的映射。怎么样:

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&lt;Converter&lt;any&gt;&gt; 而不是Array&lt;Converter&lt;any&gt;&gt;...这是一个更宽松的约束(Array 接口是ReadonlyArray 接口的扩展;每个Array 都是一个@ 987654353@ 但反之亦然;命名可能有点误导;也许应该是 ReadableAndWritableArray 代表 ArrayReadableArray 代表 ReadonlyArray... 但我离题了)。这样C 将接受typeof converters,即readonly tuple

无论如何,Names 类型别名 looks up "name" 属性类型来自number 数组的number 索引元素。而Value 类型别名Extracts C 的元素具有匹配name 属性的N,然后是infers 的值类型。

有效吗?

type TheNames = Names<typeof converters>; // "info" | "pressure"
type InfoValue = Value<typeof converters, "info">; // string
type PressureValue = Value<typeof converters, "pressure">; // number

我觉得不错。请注意,以上行没有编译器错误,因此converters 对象必须正确满足作为Converter 元素的(可能是只读的)数组的约束。

希望您可以使用它来使您的扩展示例代码正常工作。祝你好运!

Link to code

【讨论】:

  • 我有一个问题。我理解为什么const 分配是必要的,但是当我将其注释掉时,它仍然有效。我不明白,因为你说的完全有道理!
  • 无法发布链接,因为网址太长,但如果您在第 22 行执行const converters = [infoConverter, pressureConverter];// as const;,它仍然有效!
  • infoConverterpressureConverter 定义中的 const 断言很有帮助,因为没有它们,您需要其他方法来确保 "info""pressure" 名称不不扩大。但是一旦infoConverterpressureConverter 已经很窄,确实不需要在converters 定义中使用const 断言,因为所做的只是保留两个条目的顺序,而您的代码不会真的不在乎那个。我不会说as const 永远是必要的;这只是保持推理范围狭窄的最方便的方法之一。
  • 非常感谢您的解释,您是传奇!
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2020-04-12
  • 2020-12-05
  • 1970-01-01
相关资源
最近更新 更多