- Notifications
You must be signed in to change notification settings - Fork 13.2k
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
enumAxis{ROW="row",COL="col",}constAXIS_BRAND=Symbol();typeSpanId<AextendsAxis>=string&{[AXIS_BRAND]: A};typeRectangle={[AinAxis]: SpanId<A>};functiongetRectangleSide<AinAxis>(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>`returnrect[a];}functiongetRectangleSide2<AinAxis>(rect: Rectangle,a: A): Rectangle[A]{if(Math.random()>0.5){returnrect[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>`returnrect[Axis.ROW];}}Workaround
constFAKE_INDEX="fake-index";typeGenericIndex<_,K>=K|(_&typeofFAKE_INDEX);typeLooseIndex<K>=K|typeofFAKE_INDEX;enumAxis{ROW="row",COL="col",}typeAxisG<_>=GenericIndex<_,Axis>;typeAxisL=LooseIndex<Axis>;constAXIS_BRAND=Symbol();typeSpanId<AextendsAxisL>=string&{[AXIS_BRAND]: A};typeRectangle<_>={[AinAxisG<_>]: SpanId<A>};functiongetRectangleSide<_,AextendsAxis>(rect: Rectangle<_>,a: A): SpanId<A>{returnrect[a];// allowed}functiongetRectangleSide2<_,AextendsAxis>(rect: Rectangle<_>,a: A): Rectangle<_>[A]{if(Math.random()>0.5){returnrect[a];}else{returnrect[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)