TypeScript:将元组类型转换为对象

TypeScript: convert tuple type to object

总结:我有这样一个元组类型:

[session: SessionAgent, streamID: string, isScreenShare: boolean, connectionID: string, videoProducerOptions: ProducerOptions | null, connection: AbstractConnectionAgent, appData: string]

我想将它转换成这样的对象类型:

type StreamAgentParameters = {
  session: SessionAgent
  streamID: string
  isScreenShare: boolean
  connectionID: string
  videoProducerOptions: ProducerOptions | null
  connection: AbstractConnectionAgent
  appData: string
}

有办法吗?


我想为 class 的测试创建一个 factory function 以简化设置。

export type Factory<Shape> = (state?: Partial<Shape>) => Shape

我想避免手动输入 class 的参数,因此我寻找获取构造函数参数的可能性。你知道吗,有 ConstructorParameters 助手类型。不幸的是,它 returns 是一个元组而不是一个对象。

因此以下内容不起作用,因为元组不是对象。

type MyClassParameters = ConstructorParameters<typeof MyClass>
// ↵ [session: SessionAgent, streamID: string, isScreenShare: boolean, connectionID: string, videoProducerOptions: ProducerOptions | null, connection: AbstractConnectionAgent, appData: string]

const createMyClassParameters: Factory<MyClassParameters> = ({
  session = new SessionAgent(randomRealisticSessionID()),
  streamID = randomRealisticStreamID(),
  isScreenShare = false,
  connectionID = randomRealisticConnectionID(),
  videoProducerOptions = createPopulatedProducerOptions(),
  connection = new ConnectionAgent(
    new MockWebSocketConnection(),
    'IP',
    // eslint-disable-next-line @typescript-eslint/no-explicit-any
  ),
  appData = 'test',
} = {}) => ({
  session,
  streamID,
  isScreenShare,
  connectionID,
  videoProducerOptions,
  connection,
  appData,
})

我尝试创建一个将元组转换为对象的辅助类型,但我最好的尝试是这样做(但没有成功)。

type TupleToObject<T extends any[]> = {
  [key in T[0]]: Extract<T, [key, any]>[1]
}

我该如何解决这个问题?

TL;DR:不可能将元组类型转换为对象,因为元组中缺少有关键的信息。

当你说你有像 [session: SessionAgent, streamID: string] 这样的元组类型时,我猜你的意思是 [SessionAgent, string].

您不能将变量名与元组一起保留,它们会被丢弃,并且无法恢复丢失的信息。

一种解决方法,如果适合您,可以将 MyClass 构造函数签名从位置参数转换为命名参数。

// from:
class MyClass {
  constructor(session: SessionAgent, streamID: string) {…}
}

// to:
class MyClass {
  constructor(opt: { session: SessionAgent, streamID: string }) {…}
}

// now you can infer:
type MyClassParameters = ConstructorParameters<typeof MyClass>[0]
// ↵ { session: SessionAgent, streamID: string }

为了将任何元组转换为对象,您可以使用此实用程序类型:

type Reducer<
  Arr extends Array<unknown>,
  Result extends Record<number, unknown> = {},
  Index extends number[] = []
  > =
  Arr extends []
  ? Result
  : Arr extends [infer Head, ...infer Tail]
  ? Reducer<[...Tail], Result & Record<Index['length'], Head>, [...Index, 1]>
  : Readonly<Result>;

// Record<0, "hi"> & Record<1, "hello"> & Record<2, "привіт">
type Result = Reducer<['hi', 'hello', 'привіт']>;

由于我们是从元组转换的,因此您只能使用元素索引作为键。

为了保留有关 key/index 的信息,我添加了额外的 Index 通用类型来键入实用程序。每次迭代我都会添加 1 并计算 indexI

的新长度

您不能使用 tuple labels 作为密钥,因为:

They’re purely there for documentation and tooling.

如其他答案中所述,无法转换元组 labels into string literal types;标签仅用于文档,不影响类型系统:类型 [foo: string][bar: string] 以及 [string] 都是等价的。所以任何将 [foo: string] 变成 {foo: string} 的方法也应该把 [bar: string] 变成 {foo: string}。所以我们需要放弃捕获元组标签。


元组的真正键是数字字符串,如 "0"1"。如果你只是想用那些类似数字的键而不是所有的数组属性和方法将一个元组变成类似的类型,你可以这样做:

type TupleToObject<T extends any[]> = Omit<T, keyof any[]>

这只是使用 the Omit<T, K> utility type 来忽略所有数组中存在的任何元组属性(如 lengthpush 等)。这也或多或少等同于

type TupleToObject<T extends any[]> = 
  { [K in keyof T as Exclude<K, keyof any[]>]: T[K] }

明确使用 a mapped type with filtered out keys

以下是它在您的元组类型上的行为方式:

type StreamAgentObjectWithNumericlikeKeys = TupleToObject<StreamAgentParameters>
/* type StreamAgentObjectWithNumericlikeKeys = {
    0: SessionAgent;
    1: string;
    2: boolean;
    3: string;
    4: ProducerOptions | null;
    5: AbstractConnectionAgent;
    6: string;
} */

你也可以创建一个函数来对实际值做同样的事情:

const tupleToObject = <T extends any[]>(
  t: [...T]) => ({ ...t } as { [K in keyof T as Exclude<K, keyof any[]>]: T[K] });
const obj = tupleToObject(["a", 2, true]);
/* const obj: {
    0: string;
    1: number;
    2: boolean;
} */
console.log(obj) // {0: "a", 1: 2, 2: true};

如果除了类型元组之外,您还愿意保留 属性 names 的元组,您可以编写一个映射数字元组键的函数对应名称:

type TupleToObjectWithPropNames<
  T extends any[],
  N extends Record<keyof TupleToObject<T>, PropertyKey>
  > =
  { [K in keyof TupleToObject<T> as N[K]]: T[K] };
   
type StreamAgentParameterNames = [
  "session", "streamID", "isScreenShare", "connectionID",
  "videoProducerOptions", "connection", "appData"
];

type StreamAgentObject =
  TupleToObjectWithPropNames<StreamAgentParameters, StreamAgentParameterNames>
/* 
type StreamAgentObject = {
  session: SessionAgent
  streamID: string
  isScreenShare: boolean
  connectionID: string
  videoProducerOptions: ProducerOptions | null
  connection: AbstractConnectionAgent
  appData: string
}
*/

你可以创建一个函数来对实际值做同样的事情:

const tupleToObjectWithPropNames = <T extends any[],
  N extends PropertyKey[] & Record<keyof TupleToObject<T>, PropertyKey>>(
    tuple: [...T], names: [...N]
  ) => Object.fromEntries(Array.from(tuple.entries()).map(([k, v]) => [(names as any)[k], v])) as
  { [K in keyof TupleToObject<T> as N[K]]: T[K] };

const objWithPropNames = tupleToObjectWithPropNames(["a", 2, true], ["str", "num", "boo"])
/* const objWithPropNames: {
    str: string;
    num: number;
    boo: boolean;
} */
console.log(objWithPropNames); // {str: "a", num: 2, boo: true}

Playground link to code