Several obstacles are hindering your progress here.
Firstly, the order of properties within an object type is currently inconspicuous in TypeScript as it does not affect assignability. For instance, there is no distinction between the types {a: string, b: number}
and {b: number, a: string}
within the type system. Although there are techniques to extract information from the compiler to discern the key ordering like ["a", "b"]
versus ["b", "a"]
, this only provides some sorting during compilation and is not guaranteed to be maintained when reading the type declaration linearly. Refer to microsoft/TypeScript#17944 and microsoft/TypeScript#42178 for insights on this topic. It remains challenging to automatically convert an object type into an ordered tuple consistently at present.
Secondly, the names of arguments in function types are purposely hidden as string literal types due to similar reasons as with object property ordering. These names do not impact assignability within the type system. Function argument names, solely serve as documentation or IntelliSense tools. Converting between named function arguments and labeled tuple elements is feasible but has limitations. Please refer to this comment in microsoft/TypeScript#28259 for further clarification. Currently, transforming a labeled tuple into an object type where keys correlate with tuple labels cannot be automated easily.
To bypass these challenges effectively, consider providing ample information to facilitate both object to tuple and tuple to object conversions:
const userOptionKeys = ["param1", "param2", "thirdOne"] as const;
type PositionalUserOptions = [string, number, boolean];
By having userOptionKeys
identify the desired keys order in the UserOptions
objects matching that of PositionalUserOptions
, constructing UserOptions
becomes easier:
type UserOptions = { [I in Exclude<keyof PositionalUserOptions, keyof any[]> as
typeof userOptionKeys[I]]: PositionalUserOptions[I] }
/* type UserOptions = {
param1: string;
param2: number;
thirdOne: boolean;
} */
You can also create a function to convert type PositionalUserOptions
into type
UserOptions</code using <code>positionalToObj
:
function positionalToObj(opts: PositionalUserOptions): UserOptions {
return opts.reduce((acc, v, i) => (acc[userOptionKeys[i]] = v, acc), {} as any)
}
This setup will enable you to implement a User
class by utilizing positionalToObj
within the constructor to establish consistency:
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
} */
This approach functions efficiently from a type system perspective, but may lack clarity in terms of documentation and IntelliSense. To enhance parameter labels upon calling new User()
, you might need to duplicate the parameter names - once as string literals and again as tuple labels, given the current inability to convert one form to another seamlessly.
Ultimately, the decision whether or not to pursue this method depends on your specific needs and preferences.
Playground link to code