7

Summary: I have a tuple type like this:

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

and I want to convert it to an object type like this:

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

Is there a way to do that?


I want to create a factory function for tests for a class to simplify the setup.

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

I want to avoid manually typing out the parameters for the class, so I looked for possibilities to get the parameters for the constructor. And what do you know, there is the ConstructorParameters helper type. Unfortunately, it returns a tuple instead of an object.

Therefore the following doesn't work because a tuple is NOT an object.

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,
})

I tried creating a helper type that converts a tuple to an object, but my best attempt was this (and it didn't work).

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

How can I solve this problem?

3 Answers 3

8

As mentioned in the other answers, there's no way to convert tuple labels into string literal types; the labels are just for documentation and don't affect the type system: the types [foo: string] and [bar: string] and [string] are all equivalent to each other. So any method to turn [foo: string] into {foo: string} should also turn [bar: string] into {foo: string}. So we need to give up on capturing tuple labels.


The real keys of a tuple are numeric strings like "0" and 1". If you just want to turn a tuple into a similar type with just those numeric-like keys and not all the array properties and methods, you can do it like this:

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

This just uses the Omit<T, K> utility type to ignore any tuple properties that exist in all arrays (like length, push, etc). This is also more or less equivalent to

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

which uses a mapped type with filtered out keys explicitly.

Here's how it behaves on your tuple type:

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

You could also make a function to do the same thing to actual values:

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

If you are willing to hold onto an tuple of property names in addition to your tuple of types, you can write a function which maps the numeric tuple keys to the corresponding name:

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
}
*/

And you can make a function to do the same to actual values:

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

Sign up to request clarification or add additional context in comments.

1 Comment

Clever solution, as always!
5
+150

In order to convert any tuple to object, you can use this utility type:

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', 'привіт']>;

Since we are converting from the tuple you are able to use only elements indexes as a key.

In order to keep information about the key/index I have added extra Index generic type to type utility. Every iteration I'm adding 1 and compute new length of indexI

You are not allowed to use tuple labels as a key since:

They’re purely there for documentation and tooling.

5 Comments

Is there a benefit to this solution over this one: type TupleToObject<T extends any[]> = Omit<T, keyof any[]>?
@jcalz I do think that your solution is better.
So do you want me to post a separate answer or do you want to edit this one?
@jcalz please post as a separate answer
All right, here. It's another long rant as usual.
0

TL;DR: It’s impossible to convert a tuple type into an object, since information about the key is missing from the tuple.

When you say you have a tuple type like [session: SessionAgent, streamID: string], I guess you really mean [SessionAgent, string].

You don’t get to keep the variable names along side the tuple, they’re discarded, and there’s no way to restore lost information.

A workaround, if it suits you, would be converting MyClass constructor signature from positional params to named params.

// 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 }

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.