Reputation: 14766
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?
Upvotes: 6
Views: 3508
Reputation: 327964
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}
Upvotes: 7
Reputation: 33051
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 index
I
You are not allowed to use tuple labels as a key since:
They’re purely there for documentation and tooling.
Upvotes: 5
Reputation: 19957
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 }
Upvotes: 0