Skip to content

Feature: Composing spread operator on Union types and static_key function to enable generic capability #17061

Open

Description

Related issues: #13542 #15759 (slightly different take and example on closed issues)

I originally posted on issue #15759 but decided that they were different enough to be looked at separately and that issue doesn't address a generic capability. Look back at #15759 (comment) for clunky workaround examples.

Proposal

  1. Add spread operator ... to string literal union types acting as an string array or generator. Type T applying spread operator will result in type T[]. For example:
type Group = 'red' | 'blue' | 'green';
const groups = ...Group; // groups has type of Group[] with values ['red', 'green', 'blue']
  1. Add static function static_key or keyof that transforms a static interface to a string literal union of all combined types. For example:
interface Groups<T, U, V> {
  red: T;
  green: U;
  blue: V;
}
type groups = static_key(Groups); // 'red' | 'blue' | 'green'
  1. Composing both can result in powerful generic operations. I feel that this could bring a unique capability that string enums could not and may have been overlooked. It would be very powerful for a Record type or interface where all fields are the same type. Here is an example:
interface GroupDescription {
    name: string;
    description: string;
}

type Group = 'red' | 'blue' | 'green';
type Groups = { [K in Group]: GroupDescription };

function initGroups(groups: Groups) {
    for(const group of ...static_key(Groups)) { // group would be type Group or 'red' | 'blue' | 'green'
        processGroup(name); //Generically Process property
    }
}

function processGroup(group: Group) { }

I noticed I don't need ...static_key but instead directly use the spread operator on the union type ...Group. The point is that they compose well together and they add generic capability.

Activity

  1. mhegazy commented on Aug 28, 2017

    @mhegazy
    Contributor

    Seems to fall in the realm of #5453

  2. wizarrc commented on Sep 5, 2017

    @wizarrc
    Author

    #5453 (comment)

    type Union = "a" | "b"; type MyTuple = ...Union; // ["a", "b"] (order unreliable but enables iteration of unions/objects through tuples. anyway, way out of scope here.)

    Mohamed Hegazy (@mhegazy) yes, but apparently way out of scope for that effort.

  3. matheo commented on Sep 5, 2017

    @matheo

    Today I've been searching on how to extract all the values from a String Literal Union Type at runtime as an array, just like #13542 illustrates and the point 1 of this proposal.

    Currently, I handle a library with their Union Types already defined, and I need to check if my App variables belong to one of these types. It would be ideal if I could spread the type just as the point 1 example.

    Right now the only solution for me is to declare manually an array with the same values of the types I want to check at runtime, but if the library changes, my code is not synced automagically, it needs to be done manually and it's painful. That's another advantage of this kind of tools :)

    Thanks for the great stuff!

  4. ChrisTalman commented on Aug 9, 2018

    @ChrisTalman

    This would be super useful for input validation.

    For instance, a very basic Express request handler:

    type Type = 'a' | 'b' | 'c';
    
    function handler(request: ExpressRequest, response: ExpressResponse)
    {
    	const valid = (... Type).includes(response.locals.parameters.type);
    	if (valid)
    	{
    		response.sendStatus(200);
    	}
    	else
    	{
    		response.sendStatus(400);
    	};
    };

    In this example, the input, response.locals.parameters.type, is checked to see whether it is one of the allowed Type strings. This is then handled accordingly.

    Meanwhile, the type can still be used elsewhere to ensure safe code.

    function assign()
    {
    	let type: Type = 'a';
    };

    The existing alternatives to this feature are rather inelegant and at times unsafe.

    Is there any sign of this feature on the horizon?

  5. rubenpieters commented on Aug 13, 2018

    @rubenpieters

    What is the difference between static_key and the currently existing keyof?

    interface Groups<T, U, V> {
      red: T;
      green: U;
      blue: V;
    }
    type groups = keyof Groups<never, never, never>; // "red" | "green" | "blue"
  6. added
    Needs ProposalThis issue needs a plan that clarifies the finer details of how it could be implemented.
    on Nov 6, 2018
  7. renaudbardet commented on Jan 4, 2019

    @renaudbardet

    rubenpieters the difference is that keyof is in type space, meaning once the code is compiled there is no trace of the literals expressed in the union type, whereas the proposed static_keys (or ...T spread operator) is in runtime space, so those literals would be converted to an actual array that you can parse and check runtime values against

    I have been searching this issue for the same reasons as Mateo Tibaquirá (@matheo) who already explained brilliantly my concerns, current solution (synced array) and its drawbacks, here are additional considerations.

    I am using a type definition made by someone else and need parameter validation against a literal union.
    Asking this 3rd party to change their library so that those union types are expressed as enums would solve this as enums generate a double mapped dictionary at runtime.

    type T = "value1" | "value2" | "value3"
    // is type equivalent to
    enum E {
      value1,
      value2,
      value3
    }
    var paramToCheck = 'value1'
    E.hasOwnProperty( paramToCheck ) // true

    but

    • some parameter values do not convert nicely to enum members
    type T = "strange value" | "valeur étrange"
    // is type equivalent to
    enum E {
      "strange value",
      "valeur étrange"
    }
    // this works but feels odd and adds confusion with the E[number] reverse mapping convention
    const param:E = E["valeur étrange"]
    • other users of this library will get runtime values added to their codebase while they may just want the type definition, I think affecting runtime space should be my choice

    Edit: added for completeness
    Another solution would be to use the switch exhaustiveness pattern to check for the union type values, like the manually synced array solution but type safe:

    type T = "value1" | "value2" | "value3"
    function ensureT(s:string): T {
      let t: T = s as T
      switch(t) {
        case 'value1':
        case 'value2':
        case 'value3':
          return t
        default: (function(x:never):never{
                        throw new Error(`unexpected value for T: ${s}`)
                      }(t))
       }
    }

    By assuming our parameter is a valid T and passing it through a (never)=>never in the default case, the type checker will complain if the switch is not exhaustive, which ensures synchronisation with the type definition.
    At runtime, t = s as T will actually let pass any wrong value, which will be caught in the default case of the switch and trigger the error throw.

  8. redbaron commented on Aug 13, 2019

    @redbaron

    Having this feature would allow type safe iteration over Record<> which currently is not practical

  9. TomasHubelbauer commented on Oct 19, 2019

    @TomasHubelbauer

    This feature would enable getting function argument names at runtime, which is a problem usually solved with regexes or AST parsers in JavaScript (see for example Angular's injector implementation).

    More than that, it would allow to get runtime function argument names from a function type.

    So, having a type like:

    {
      createPerson: (firstName: string, lastName: string, age: number) => Promise<Person>;
    }

    One could write an API gateway using ES Proxy which would intercept method calls (using the apply trap) and use the argsArray trap argument in combination with this proposed functionality for obtaining the argument names at runtime to build an object, such as this:

    {
      firstName: "John",
      lastName: "Doe",
      age: 23
    }
    

    Which could then be stringified and fetch-ed against an API.

    Currently, doing this is not possible, because there is no way to get the function argument names at runtime. Since in this case a proxy is used for the implementation, there are no actual functions in the runtime either. If there were, one could use a regex or an ES parser to parse out the argument names from the function's string representations, but the reason I'm showing this particular problem is that there are potentially very useful use-cases this feature would enable which are not possible currently.

    I mean, how could would that be to have an automagical proxy-based API gateway based solely on a single API type?

  10. mblaze commented on Dec 7, 2020

    @mblaze

    Converting literal types to values is something that other languages already do and it would be nice to have that in TS as well. IMO a valueof keyword would be a good choice for that as it could have a more general usecase.

    Example:

    const a: 'a' = valueof 'a'
    console.log(a) // 'a'
    const s: ['a', 'b', 'c'] = valueof ['a', 'b', 'c']
    console.log(s) // ['a', 'b', 'c']

    The only thing missing then would be a conversion from a literal union to a literal tuple but I believe that this is already possible with some advanced typing.

    type UnionToTuple<U> = ???
    type Sex = 'male' | 'female'
    const sexes: Array<Sex> = valueof UnionToTuple<Sex>
    console.log(sexes) // ['male', 'female']

    I'd really love to see this feature in TypeScript as it would allow for generating arbitrary values and functions based on types (validation, reducers, etc.).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    Needs ProposalThis issue needs a plan that clarifies the finer details of how it could be implemented.SuggestionAn idea for TypeScript

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions