question-mark
Stuck on an issue?

Lightrun Answers was designed to reduce the constant googling that comes with debugging 3rd party libraries. ItĀ collects links to all the places you might be looking at while hunting down a tough bug.

And, if youā€™re still stuck at the end, weā€™re happy to hop on a call to see how we can help out.

Support C# / Rust-style "where" syntax for generic parameters

See original GitHub issue

Suggestion

šŸ” Search Terms

  • rust where

āœ… Viability Checklist

  • This wouldnā€™t be a breaking change in existing TypeScript/JavaScript code: this would introduce new syntax that would currently be an error.
  • This wouldnā€™t change the runtime behavior of existing JavaScript code: this would be a purely type-level construct
  • This could be implemented without emitting different JS based on the types of the expressions: this would get erased at runtime
  • This isnā€™t a runtime feature (e.g. library functionality, non-ECMAScript syntax with JavaScript output, new syntax sugar for JS, etc.): this would be a purely type-level feature
  • This feature would agree with the rest of TypeScriptā€™s Design Goals. Itā€™s more inspired by the syntax in Rust than attempting to replicate it

ā­ Suggestion

Instead of writing:

function getProperty<T, K extends keyof T>(obj: T, key: K) {
  return obj[key];
}

Youā€™d also be allowed to write:

function getProperty<T, K>(obj: T, key: K)
  where
  T extends object,
  K extends keyof T
{
  return obj[key];
}

It might even be preferable to leave T and K off the generic parameters list (the bit inside <..>), since the intent is most likely for them to be inferred based on obj and key, but weā€™ll get to this later.

For a type alias, instead of:

type Pick<T extends object, K extends keyof Type> = {[k in K]: T[k]};

youā€™d also be able to write:

type Pick<Type, Keys> = {[K in Keys]: Type[K]}
  where
  Type extends object,
  Keys extends keyof Type;

This mirrors an identical syntax in Rust (see also Rust RFC 135) (update: and also C#, so I guess Anders knows about this!). It would solve three distinct problems:

  1. Legibility When a function or type alias has many generic arguments, each with an extends clause and a default value, it can get difficult to pick out what the type parameters are, or even how many of them there are. A where clause lets you push the generic bounds and defaults outside the parameter list, improving its legibility.

  2. Scoped type aliases Thereā€™s no easy type-level equivalent of factoring out a variable to eliminate duplicated expressions like you would in JavaScript. A where clause would make it possible to introduce type aliases that donā€™t appear in the generic parameter list.

  3. Partial inference for functions See #10571. Itā€™s not currently possible to specify one type parameter for a generic function explicitly but allow a second one to be inferred. By creating a place to put types thatā€™s not the parameter list, a where clause would make this possible.

There are examples of all three of these in the next section.

šŸ’» Use Cases

Legibility

There are many examples of complicated generic parameter lists out there. Hereā€™s one chosen at random from react-router:

export interface RouteChildrenProps<Params extends { [K in keyof Params]?: string } = {}, S = H.LocationState> {
    history: H.History;
    location: H.Location<S>;
    match: match<Params> | null;
}

Itā€™s clearer that there are two type parameters if you move the constraints and default values out of the way:

export interface RouteChildrenProps<Params, S> where
  Params extends { [K in keyof Params]?: string } = {},
  S = H.LocationState
{
    history: H.History;
    location: H.Location<S>;
    match: match<Params> | null;
}

The existing workaround for this is to put each type parameter on its own line:

export interface RouteChildrenProps<
  Params extends { [K in keyof Params]?: string } = {},
  S = H.LocationState
> {
    history: H.History;
    location: H.Location<S>;
    match: match<Params> | null;
}

Itā€™s a judgment call which you prefer. I find the other two uses more compelling!

Scoped type aliases

With complicated generic types and functions, itā€™s common to have repeated type expressions. Hereā€™s a particularly egregious example (source):

import * as express from 'express';
/** Like T[K], but doesn't require K be assignable to keyof T */
export type SafeKey<T, K extends string> = T[K & keyof T];
export type ExtractRouteParams<Path extends string> = ...;  // see https://twitter.com/danvdk/status/1301707026507198464

export class TypedRouter<API> {
  // ...
  get<
    Path extends keyof API & string,
  >(
    route: Path,
    handler: (
      params: ExtractRouteParams<Path>,
      request: express.Request<ExtractRouteParams<Path>, SafeKey<SafeKey<API[Path], 'get'>, 'response'>>,
      response: express.Response<SafeKey<SafeKey<API[Path], 'get'>, 'response'>>,
    ) => Promise<SafeKey<SafeKey<API[Path], 'get'>, 'response'>>
  ) {
    // ... implementation ...
  }
}

wow thatā€™s a lot of repetition! Hereā€™s what it might look like with where:

export class TypedRouter<API> {
  // ...
  get<Path extends keyof API & string>(
    route: Path,
    handler: (
      params: Params,
      request: express.Request<Params, Response>,
      response: express.Response<Response>,
    ) => Promise<Response>
  )
  where
    Params = ExtractRouteParams<Path>,
    Spec = SafeKey<API[Path], 'get'>,
    Response = SafeKey<Spec, 'response'>
  {
    // ... implementation ...
  }
}

By introducing some local type aliases in the where list, weā€™re able to dramatically reduce repetition and improve clarity. We should actually remove Path from the type parameters list since the intention is for it to be inferred, but letā€™s save that for the next example.

Existing workarounds include factoring out more helper types to reduce duplication, or introducing an extra generic parameter with a default value, e.g.:

class TypedRouter<API> {
  // ...
  get<
    Path extends keyof API & string,
    Spec extends SafeKey<API[Path], 'get'> = SafeKey<API[Path], 'get'>,
  >(
    route: Path,
    handler: (
      params: ExtractRouteParams<Path>,
      request: express.Request<ExtractRouteParams<Path>, SafeKey<Spec, 'response'>>,
      response: express.Response<SafeKey<Spec, 'response'>>,
    ) => Promise<SafeKey<Spec, 'response'>>
  ) {
    // ... implementation ...
  }
}

This still repeats the type expression twice (SafeKey<API[Path], 'get'>), but since itā€™s used three times, itā€™s a win! This is kinda gross and creates confusion for users about whether Spec is a meaningful generic parameter that theyā€™d ever want to set (it isnā€™t).

Partial inference for functions

Sometimes you want to infer one generic parameter to a function and have the others be derived from that (#10571). For example (following this post):

declare function getUrl<
  API, Path extends keyof API
>(
  path: Path, params: ExtractRouteParams<Path>
): string;

This fails if you pass API explicitly and try to let Path be inferred:

getUrl<API>('/users/:userId', {userId: 'bob'});
//     ~~~ Expected 2 type arguments, but got 1. (2558)

This problem could be solved by using where to introduce a type parameter thatā€™s not part of the generics list:

declare function getUrl<API>(path: Path, params: ExtractRouteParams<Path>): string
  where Path extends keyof API;

This would allow Path to be inferred from the path parameter while still specifying API explicitly and enforcing the extends keyof API constraint. The only workarounds Iā€™m aware of now involve introducing a class or currying the function to create a new binding site:

declare function getUrl<API>():
  <Path extends keyof API>(
    path: Path,
    params: ExtractRouteParams<Path>
  ) => string;

A where clause would help with the general problem that there are two reasons to put a type parameter in the generic parameters list for a function:

  1. You want users to specify it
  2. You want it to be inferred

and thereā€™s no syntax for distinguishing those. A where clause would let you do that.

Issue Analytics

  • State:open
  • Created 3 years ago
  • Reactions:28
  • Comments:12 (5 by maintainers)

github_iconTop GitHub Comments

3reactions
RyanCavanaughcommented, Jan 20, 2021

I donā€™t love the syntactic positioning of this or the idea that it would be a place to introduce type parameters, but Iā€™m compelled by this as an alternative approach to some other feature requests (specifically #14520 upper-bounded generics). You could imagine writing things like this

interface Array<T> {
  // Proposing that "where" go at the end of the type parameter list
  includes<U where T extends U>(el: U): boolean
}
2reactions
danvkcommented, Jan 20, 2021

Also, if the where clauses didnā€™t influence inference at all, which I think would be desirable, then you could solve non-inferential type parameter (#14829) use cases:

declare function assertEqual<T, U where U extends T, T extends U>(actual: T, expected: U): boolean;
const g = { x: 3, y: 2 };
// Would correctly error
assertEqual(g, { x: 3 });

Or perhaps

declare function assertEqual<T, U where [U, T] extends [T, U]>(actual: T, expected: U): boolean;
Read more comments on GitHub >

github_iconTop Results From Across the Web

Generic Data Types - The Rust Programming Language
The syntax for using generics in struct definitions is similar to that used in function definitions. First, we declare the name of the...
Read more >
Using Generic Types in Rust - Matt Oswalt
In this post, we'll cover the syntax for using generics in Rust, with some practical examples. We'll cover the underlying implementation inĀ ...
Read more >
Understanding Rust generics and how to use them
Syntax for Rust generic type parameters; Simple Rust generic usage example; Generics with trait bounds; Lifetime generics in Rust; TypestateĀ ...
Read more >
[RFC] Lifetime annotations for C++ - Clang Frontend
Lifetime annotations can help provide safer C++/Swift interoperability. ... C++23's explicit object parameter syntax ((P0847R7)[DeducingĀ ...
Read more >
Rust Lang in a nutshell: 3# Traits and generics - Softax
This post is about Traits and Generics. ... cargo clippy or reformat code in place according to Rust style guides with help of...
Read more >

github_iconTop Related Medium Post

No results found

github_iconTop Related StackOverflow Question

No results found

github_iconTroubleshoot Live Code

Lightrun enables developers to add logs, metrics and snapshots to live code - no restarts or redeploys required.
Start Free

github_iconTop Related Reddit Thread

No results found

github_iconTop Related Hackernoon Post

No results found

github_iconTop Related Tweet

No results found

github_iconTop Related Dev.to Post

No results found

github_iconTop Related Hashnode Post

No results found