Description
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
mapped type indexed access type lookup type substitute substitution generic
Suggestion
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
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. 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
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
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:
- This wouldn't be a breaking change in existing TypeScript / JavaScript code
- This wouldn't change the runtime behavior of existing JavaScript code
- This could be implemented without emitting different JS based on the types of the expressions
- This isn't a runtime feature (e.g. new expression-level syntax)