Positional vs Named Parameters in TypeScript Constructor

Currently, I have a class that requires 7+ positional parameters.

class User {
  constructor (param1, param2, param3, …etc) {
    // …
  }
}

I am looking to switch to named parameters using an options object.

type UserOptions = {
  param1: string
  // …
}

class User {
  constructor ({ param1, param2, param3, …etc } = {}: UserOptions) {
    // …
  }
}

While this change is good, it means all tests must be updated to accommodate the new signature. Therefore, I would like to support both named and positional parameters.

I can implement code to handle both scenarios, but I'm unsure how to include types without duplicating them. Ideally, the types in my UserOptions should translate into positional parameters based on their order of definition in UserOptions.

Is there a way to achieve this?

type UserOptions = {
  param1: string
  // …
}

type ToPositionalParameters<T> = [
  // ??? something like ...Object.values(T)
  // and somehow getting the keys as the positional argument names???
]

type PositionalUserOptions = ToPositionalParameters<UserOptions>

class User {
  constructor (...args: PositionalUserOptions);
  constructor (options: UserOptions);
  constructor (...args: [UserOptions] | PositionalUserOptions) { 
    // …
  }
}

I am open to considering a solution working in reverse i.e. from positional arguments to named ones, if that would be easier?

Answer №1

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

Answer №2

It is possible to achieve this, although it's worth noting that the tools used are not recommended by the TypeScript team and rely on the order of instantiation for specific internal types (as indicated in the discussion regarding a union-to-tuple operation).

Despite this, it was an interesting challenge to solve, so here is the solution:

// Adapted from https://github.com/Microsoft/TypeScript/issues/13298#issuecomment-707364842
type UnionToTuple<T> = (
    (
        (
            T extends any
                ? (t: T) => T
                : never
        ) extends infer U
            ? (U extends any
                ? (u: U) => any
                : never
            ) extends (v: infer V) => any
                ? V
                : never
            : never
    ) extends (_: any) => infer W
        ? [...UnionToTuple<Exclude<T, W>>, W]
        : []
);

type Head<T> = T extends [infer A, ...any] ? A : never
type Tail<T extends any[]> = T extends [any, ...infer A] ? A : never;

type PluckFieldTypes<T extends object, Fields extends any[] =
  UnionToTuple<keyof T>> = _PluckFieldType<T, Head<Fields>, Tail<Fields>, []>
type _PluckFieldType<
  T extends object,
  CurrentKey,
  RemainingKeys extends any[],
  Result extends any[]
  > = RemainingKeys['length'] extends 0
      ? CurrentKey extends keyof T ? [...Result, T[CurrentKey]] : never
      : CurrentKey extends keyof T
        /* && */? RemainingKeys extends (keyof T)[]
        ? [...Result, T[CurrentKey], ..._PluckFieldType<T, Head<RemainingKeys>, Tail<RemainingKeys>, []>]
        : never : never;

// -- IMPLEMENTATION --
type Args = {
  param1: string,
  param2: number,
  param3: Date,
  param4: number,
  param5: string,
  param6: Date,
  param7: boolean,
  param8: boolean,
  param9: null,
  param10: 'abc' | 'xyz'
}

class CompositeClass {
  constructor(params: Args);
  constructor(param1: string, param2: number, param3: Date);
  constructor(...args: [Args] | PluckFieldTypes<Args>) {
  }
}

The type inferred for ...args ends up being

(parameter) args: [Args] | [string, number, Date, number, string, Date, boolean, boolean, null, "abc" | "xyz"]
and the second constructor shows an error until all other parameters are included.

The one limitation we encounter is the inability to enforce the argument names matching the field names (due to labeled tuples not exposing labels at the type level, making them inaccessible through built-in constructs like ConstructorArguments or within this context using _PluckFieldType.)

Similar questions

If you have not found the answer to your question or you are interested in this topic, then look at other similar questions below or use the search

Open the .exe file using an HTML link in Firefox browser

While working on a project using php / js / html, I encountered the need to launch a C# application from my web page without requiring any downloads. The challenge was that I primarily use Mozilla Firefox and could only find solutions involving ActiveXOb ...

Modifying HTML line codes using Python: A step-by-step guide

If only I could modify this particular line: <button _ngcontent-c19="" class="blue-button-disabled" disabled="">CONTINUE </button> to be like this instead: <button _ngcontent-c19="" class="blue- ...

What is the best way to retrieve a value from $http or $resource using this filter?

Within my HTML, I have a structure that looks like {{ i.userId | userName }}. Essentially, I am attempting to convert this into a name by utilizing a filter. However, I am encountering numerous challenges with the asynchronous function required to retrieve ...

Navigating through the nested object values of an Axios request's response can be achieved in React JS by using the proper

I am attempting to extract the category_name from my project_category object within the Axios response of my project. This is a singular record, so I do not need to map through an array, but rather access the entire object stored in my state. Here is an ex ...

Using the JavaScript JSX syntax, apply the map function to the result of a

My aim is to create a structure resembling the following: <td> {phones.map((phone, j) => <p>{this.renderPhone(phone)}</p> )} </td> However, there may be instances where the phones array is not defined. Is it feas ...

objects bound to knockout binding effects

I have been struggling to understand why the binding is not working as expected for the ‘(not working binding on click)’ in the HTML section. I have a basic list of Players, and when I click on one of them, the bound name should change at the bottom of ...

Gaining access to the isolated scope of a sibling through the same Angular directive led to a valuable discovery

I am currently working on an angularjs directive that creates a multi-select dropdown with a complex template. The directives have isolated scopes and there is a variable called open in the dropdown that toggles its visibility based on clicks. Currently, t ...

Unusual behavior: Django app not triggering Ajax XHR onload function

I'm currently working on a Django app that features user posts, and I'm in the process of implementing a 'like' / voting system. Initially, I set up this functionality using complete page refreshes combined with a redirect after the vot ...

Is there a feature similar to Google/YouTube Insight called "Interactive PNGs"?

Recently, I have been exploring the YouTube Insight feature and I am intrigued by how their chart PNGs are generated. When you view the statistics for a video, the information is presented in interactive PNG images. These images seem to be entirely compos ...

Time whizzes by too swiftly

After attempting to create an automated slideshow with clickable buttons, I encountered a problem. The slideshow functions properly, but as soon as I click one of the buttons, the slideshow speeds up significantly. This is what I implemented in my attempt ...

Tips for effectively utilizing the display: inline property in conjunction with ng-repeat

I am struggling to create a timeline with a broken structure on my website. I suspect that the issue lies in using display:inline. When attempting to apply this to my site: https://i.stack.imgur.com/4Ur7k.png the layout gets distorted: https://i.stack.i ...

How can I use lodash to locate and include an additional key within an array object?

I am looking to locate and insert an additional key in an object within an array using lodash. I want to avoid using a for loop and storing data temporarily. "book": [ { "name": "Hulk", "series": [ { "name": "mike", "id": " ...

Preventing an infinite re-render loop caused by event listeners in React

One functional component is causing me some trouble: export default function Nav({photo}) { const [isOpen, setIsOpen] = useState(false) const [width, setWidth] = useState(window.innerWidth); const breakpoint = 768; useEffect(() => { ...

adjustable canvas dimensions determined by chosen images

I would like to create a canvas image based on the selected image <canvas id="canvas" ></canvas> <input type="file" id="file-input"> Using JavaScript: $(function() { $('#file-input').change(function(e) { var file ...

NodeJs Classes TypeError: Unable to access 'name' property as it is undefined

I am currently developing a useful API using Node.js. In order to organize my code effectively, I have created a UserController class where I plan to include all the necessary methods. One thing I am struggling with is how to utilize a variable that I set ...

Error: ...[i] has not been defined

What seems to be the issue with this part of the script: function refreshLabels() { // loop through all document elements var allnodes = document.body.getElementsByTagName("*"); for (var i=0, max=allnodes.leng ...

Retrieving data from handlebars variable in a client-side JavaScript file

When creating a handlebars view using hbs for the express js framework, I am faced with an issue of accessing the variables passed to the view from a separate JavaScript file. Here's an example: var foo = {{user.name}} This code obviously results ...

How can we merge all the values of keys within an object into a new array using ES6?

My objective is to transform dynamic data into an array based on the keys of the toolset object, which does not have a constant number of keys. { toolset:{ info1:{ event-date:{}, event-time:{}, }, info ...

Using State.go in JavaScript involves waiting for it to finish before proceeding

After implementing a JS code snippet as shown below: exports.openTab = function(location) { var $state = app.getInjector().get( '$state' ); var opts = {}; var toParams = {}; toParams.id = "myPage"; $state.g ...

Accessing a file url with Firefox using protractor

I am currently facing a challenge with Protractor as I try to access an HTML file as a website using it. Whenever I attempt to do so, I encounter an error from Protractor stating: Failed: Access to 'file:///C:/filelocation/index.html' from s ...