Repository: TypeScript
Issue: Allow type variables to be constrained singleton, causing lookup into a non-generic mapped type to substitute
Labels: Suggestion,In Discussion

<!-- 🚨 STOP 🚨 𝗦𝗧𝗢𝗣 🚨 𝑺𝑻𝑶𝑷 🚨

Half of all issues filed here are duplicates, answered in the FAQ, or not appropriate for the bug tracker. Please read the FAQ first, especially the "Common Feature Requests" section.

-->

I feel awkward submitting this suggestion since I don't know if it will get enough votes to go anywhere, but I guess _someone_ has to be the initial submitter for each suggestion...

## Search Terms

<!-- List of keywords you searched for before creating this issue. Write them down here so that others can find this suggestion more easily -->
mapped type indexed access type lookup type substitute substitution generic

## Suggestion

<!-- A summary of what you'd like to see added or changed -->

Currently, a lookup into a mapped type, for example `{ [P in K]: Box<T[P]> }[X]`, is simplified by substitution (in this example, to produce `Box<T[X]>`) only if the constraint type `K` is generic; this is unsound but I guess it was useful in some cases.  I'd like to be able to constrain a type parameter `X` to be a singleton type, causing substitution to occur (which is sound) regardless of whether `K` is generic.

## Use Case

<!--
What do you want to use this for?
What shortcomings exist with current approaches?
-->
Suppose we have a codebase with an enum `E` and many functions that simulate dependent types by taking a type parameter `A extends E` (where `A` is intended to be a singleton type) along with a value of type `A`.  Given a generic type `T<A extends E>`, we may want an object that contains a `T<A>` for each `A` in `E`, i.e., `{[A in E]: T<A>}`.  Then we'd like to pass this object to a function along with a particular choice of `A` and have it manipulate the corresponding property.  We should get a type error if the function uses the wrong property.  Currently, a lookup type expression like `{[A in E]: T<A>}[A1]` does not substitute (because the constraint type `E` is not generic), so all reads and writes to the property are checked using the constraint of the lookup type, which is `{[A in E]: T<A>}[E]`, and in effect we get no distinction among the properties of the object.

Specifically, I'm writing a structured spreadsheet tool that [manipulates row and column IDs](https://bitbucket.org/espalier-spreadsheet/espalier/src/5a582831912434caaf703f6dcc5b9e18d7dfa2c7/src/layout/core.ts?at=master&fileviewer=file-view-default).  A rectangle ID is a pair of a row ID and a column ID.  I wanted to brand the row and column IDs differently to ensure I don't mix them up.  I have many functions that are parameterized over an axis: for example, `getRectParentOnAxis` takes a rectangle and can either find the rectangle that covers the same column and a larger row, or the same row and a larger column.

One current approach, which I've taken and I call the "generic index" hack, is to add an artificial type variable to every relevant type and function so that I can ensure the constraint type of the mapped type is always generic and the mapped type will always substitute.  (See "Workaround" below.)  This is ugly, but I wanted the checking badly enough to do it.

## Examples
<!-- Show how this would be used and what the behavior would be -->
```ts
enum Axis {
    ROW = "row",
    COL = "col",
}
const AXIS_BRAND = Symbol();
type SpanId<A extends Axis> = string & {[AXIS_BRAND]: A};

type Rectangle = {[A in Axis]: SpanId<A>};

function getRectangleSide<A in Axis>(rect: Rectangle, a: A): SpanId<A> {
    // Error with `A extends axis`: `Rectangle[A]` doesn't simplify and isn't assignable to `SpanId<A>`
    // Allowed with `A in Axis`: `Rectangle[A]` simplifies to `SpanId<A>`
    return rect[a];
}
function getRectangleSide2<A in Axis>(rect: Rectangle, a: A): Rectangle[A] {
    if (Math.random() > 0.5) {
        return rect[a];
    } else {
        // Allowed with `A extends axis`: `SpanId<Axis.ROW>` is unsoundly assignable to `Rectangle[A]`
        // because it is assignable to the constraint `SpanId<Axis.ROW> | SpanId<Axis.COL>`
        // Error with `A in Axis`: `SpanId<Axis.ROW>` is not assignable to `SpanId<A>`
        return rect[Axis.ROW];
    }
}
```

## Workaround
```ts
const FAKE_INDEX = "fake-index";
type GenericIndex<_, K> = K | (_ & typeof FAKE_INDEX);
type LooseIndex<K> = K | typeof FAKE_INDEX;

enum Axis {
    ROW = "row",
    COL = "col",
}
type AxisG<_> = GenericIndex<_, Axis>;
type AxisL = LooseIndex<Axis>;
const AXIS_BRAND = Symbol();
type SpanId<A extends AxisL> = string & {[AXIS_BRAND]: A};

type Rectangle<_> = {[A in AxisG<_>]: SpanId<A>};
function getRectangleSide<_, A extends Axis>(rect: Rectangle<_>, a: A): SpanId<A> {
    return rect[a];  // allowed
}
function getRectangleSide2<_, A extends Axis>(rect: Rectangle<_>, a: A): Rectangle<_>[A] {
    if (Math.random() > 0.5) {
        return rect[a];
    } else {
        return rect[Axis.ROW];  // error
    }
}
```

## Checklist

My suggestion meets these guidelines:
* [X] This wouldn't be a breaking change in existing TypeScript / JavaScript code
* [X] This wouldn't change the runtime behavior of existing JavaScript code
* [X] This could be implemented without emitting different JS based on the types of the expressions
* [X] This isn't a runtime feature (e.g. new expression-level syntax)