Repository: TypeScript
Issue: Support C# / Rust-style "where" syntax for generic parameters
Labels: Suggestion,In Discussion

# Suggestion

## 🔍 Search Terms

- rust where

## ✅ Viability Checklist

* [x] This wouldn't be a breaking change in existing TypeScript/JavaScript code: _this would introduce new syntax that would currently be an error._
* [x] This wouldn't change the runtime behavior of existing JavaScript code: _this would be a purely type-level construct_
* [x] This could be implemented without emitting different JS based on the types of the expressions: _this would get erased at runtime_
* [x] 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_
* [x] This feature would agree with the rest of [TypeScript's Design Goals](https://github.com/Microsoft/TypeScript/wiki/TypeScript-Design-Goals). _It's more **inspired** by the syntax in Rust than attempting to replicate it_

## ⭐ Suggestion

Instead of writing:

```ts
function getProperty<T, K extends keyof T>(obj: T, key: K) {
  return obj[key];
}
```

You'd also be allowed to write:

```ts
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:

```ts
type Pick<T extends object, K extends keyof Type> = {[k in K]: T[k]};
```

you'd also be able to write:

```ts
type Pick<Type, Keys> = {[K in Keys]: Type[K]}
  where
  Type extends object,
  Keys extends keyof Type;
```

This mirrors [an identical syntax in Rust][where] (see also [Rust RFC 135][where-rfc]) (**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

<!--
  What do you want to use this for?
  What shortcomings exist with current approaches?
  What workarounds are you using in the meantime?
-->

### Legibility

There are many examples of complicated generic parameter lists out there. Here's one chosen at random from [react-router][rr]:

```ts
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:

```ts
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:

```ts
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][crosswalk-rep]):

```ts
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`:

```ts
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.:

```ts
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][gentips-1]): 

```ts
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:

```ts
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:

```ts
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:

```ts
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.

[where]: https://doc.rust-lang.org/rust-by-example/generics/where.html
[where-rfc]: https://github.com/rust-lang/rfcs/blob/master/text/0135-where.md
[gentips-1]: https://effectivetypescript.com/2020/12/04/gentips-1-curry/
[crosswalk-rep]: https://github.com/danvk/crosswalk/blob/e6a32da562af71402e7e7c3ea295a19dc40e4fde/src/typed-router.ts#L41-L57
[rr]: https://github.com/DefinitelyTyped/DefinitelyTyped/blob/0f72c2cfe30d3e38e0ba4d9e5959e1baa1b1f660/types/react-router/index.d.ts#L80-L84
[c#]: https://docs.microsoft.com/en-us/dotnet/csharp/language-reference/keywords/where-generic-type-constraint