TypeScript:数组中项的特定属性的并集

时间:2019-09-05 19:42:17

标签: typescript

  

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"); 作为参数,它将告诉我只能传递convertersinfo。我根本不传递任何转换器(pressure)我可以传递任何数字或字符串。

但这只是假的,因为实现

service2

为此,这是我无法弄清的部分。

我需要告诉type Name<C extends Converters> = "???"; type Value<C, N extends NameOrCUUID<C>> = "???"; 允许TypeScript中任何项目的name属性都带有值,并且值必须与名称匹配。

对不起,我在此留言。我希望我能表达我的要求(最后一段是实际的问题)。我不知道在TypeScript中是否有可能。

这是一个游乐场:

This elaborate example on typescriptlang.org/play

1 个答案:

答案 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,但反之则不;名称可能有点误导;对于ReadonlyArrayArray,它应该是ReadonlyArray ReadableAndWritableArray ...,但我离题了。这样Array将接受readonly tupleReadableArray

无论如何,ReadonlyArray数组的C索引元素中的typeof converters类型别名looks upNames属性类型。 "name"类型别名Extractsnumber元素具有C属性与Value匹配的元素,然后infers从中得到的值类型

行得通吗?

C

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

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

Link to code