There are a few things blocking you here.
The first is that the order of properties in an object type is currently unobservable in TypeScript, because they don't affect assignability. For example, there is no difference between the type {a: string, b: number} and {b: number, a: string} in the type system. There are dirty tricks you could perform to tease information out of the compiler to see if it represents the keys as ["a", "b"] vs ["b", "a"], but all that does is give you some ordering when you compile; it's not guaranteed to be ordering when you read the type declaration from top to bottom... heck, it's not even guaranteed to be the same ordering every time you compile! See microsoft/TypeScript#17944 for an open issue to ask that there be a consistent ordering. And see microsoft/TypeScript#42178 for an example of a seemingly irrelevant code change which alters the ordering from what is expected. For now, we can't automatically turn an object type into an ordered tuple in any consistent way.
The second is that the names of arguments in a function type are intentionally not available as string literal types. They are unobservable, for similar reasons as with object property ordering: they don't affect assignability. For example, there is no difference between the function type (foo: string) => void and (bar: string) => void in the type system. I don't even think there are any tricks that could expose such details. In the type system, function argument names are only useful as documentation or IntelliSense. You can convert between named function arguments and labeled tuple elements, but that's about it. See this comment in microsoft/TypeScript#28259 explaining that we can't use call signature parameter names or tuple labels as strings in TypeScript. For now, we can't automatically turn a labeled tuple into an object type where the keys of the object correspond to the labels of the tuple.
You can kind of sidestep both of these issues if, instead of trying to convert an object to a tuple or a tuple to an object, we provide enough information to do both:
const userOptionKeys = ["param1", "param2", "thirdOne"] as const;
type PositionalUserOptions = [string, number, boolean];
Here userOptionKeys is an explicit ordered list of the desired keys in the UserOptions objects... the same order as in our manually-created PositionalUserOptions tuple. Now that we have key names, we can build UserOptions:
type UserOptions = { [I in Exclude<keyof PositionalUserOptions, keyof any[]> as
typeof userOptionKeys[I]]: PositionalUserOptions[I] }
/* type UserOptions = {
param1: string;
param2: number;
thirdOne: boolean;
} */
And while we're at it, we can write a function to convert a tuple of type PositionalUserOptions into an object of type UserOptions (with an any type assertion to free the compiler from having to try to verify it, which it can't do easily):
function positionalToObj(opts: PositionalUserOptions): UserOptions {
return opts.reduce((acc, v, i) => (acc[userOptionKeys[i]] = v, acc), {} as any)
}
Now we can write that User class, using positionalToObj in the implementation of the constructor to normalize things:
class User {
constructor(...args: PositionalUserOptions);
constructor(options: UserOptions);
constructor(...args: [UserOptions] | PositionalUserOptions) {
const opts = args.length === 1 ? args[0] : positionalToObj(args);
console.log(opts);
}
}
new User("a", 1, true);
/* {
"param1": "a",
"param2": 1,
"thirdOne": true
} */
It works! From a type system and assignability standpoint, this is the best you can do. From a documentation/IntelliSense perspective, it's not great. When you call new User(), the documentation for the multi-param version will give you labels like args_0 and args_1. If you want those to be param1 and param2, you'll just have to bite the bullet and write out the parameter names twice; once as string literals, and again as tuple labels, since there's no way to convert one to the other:
const userOptionKeys = ["param1", "param2", "thirdOne"] as const;
type PositionalUserOptions = [param1: string, param2: number, thirdOne: boolean];
Is it worth it? Maybe... that's up to you.
Playground link to code