microsoft / microsoft/TypeScript

Allow type variables to be constrained singleton, causing lookup into a non-generic mapped type to substitute

Abierto
#25,879 2 comentarios 6 reacciones 0 asignados Ver en GitHub

Nadie ha tomado este issue todavía.

In Discussion Suggestion
Lenguaje dominante
Go
Estrellas
111k
Forks
14.4k
Merge medio
1 d 19 h
PR fusionados (30 d)
117

Descripción

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)

Guía de contribución

Abrir la guía de contribución

Primeros pasos

  1. Lee el issue completo y luego la guía de contribución del proyecto.
  2. Comenta en el issue que vas a ocuparte — evita que dos personas hagan lo mismo.
  3. Haz un fork del repositorio y trabaja en una rama.
  4. Abre un pull request que haga referencia al número del issue.

Línea de trabajo

Empieza reproduciendo los ejemplos de búsqueda de mapped types mostrados en el issue, incluidos los casos de workaround de Rectangle y GenericIndex, e inspecciona cómo se comprueban actualmente las variables de tipo restringidas a singleton. Se considera terminado cuando se realiza la sustitución de búsqueda solicitada para las restricciones singleton, mientras que el acceso incorrecto a la propiedad mostrado sigue produciendo un error.

Escrito por el modelo de indexación a partir del texto del issue.

Evaluación

Stack tecnológico
typescript
Área
compilers
Tipo de issue
Nueva funcionalidad
Dificultad
5/5
Tiempo estimado
Más de una semana
Estado de actividad
Estancado
Claridad
Bastante claro
Aptitud para principiantes
30/100

Recibe los nuevos issues en tu correo

Un resumen breve de issues de GitHub para principiantes.