microsoft / microsoft/TypeScript
Allow type variables to be constrained singleton, causing lookup into a non-generic mapped type to substitute
まだ誰も着手していません。
- 主要言語
- Go
- スター
- 111k
- フォーク
- 14.3k
- 平均マージ
- 1日 19時間
- マージ済み PR(30日)
- 117
説明
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)
コントリビューションガイド
はじめの一歩
- issue を最後まで読み、次にプロジェクトのコントリビューションガイドを読みます。
- 着手することを issue にコメントします — 二人が同じ作業をするのを防げます。
- リポジトリをフォークし、ブランチを切って変更します。
- issue 番号を参照したプルリクエストを送ります。
調査の方向性
まず、issue に示されている mapped type の lookup 例を、Rectangle と GenericIndex の workaround ケースを含めて再現し、singleton に制約された型変数が現在どのようにチェックされているかを調べます。singleton 制約に対して要求された lookup の置換が行われ、示されている誤ったプロパティアクセスが引き続きエラーになることを確認できれば完了です。
索引モデルが issue の本文から書いたものです。
評価
- 技術スタック
- typescript
- 領域
- compilers
- issue の種類
- 機能追加
- 難易度
- 5/5
- 見積もり時間
- 1週間以上
- 活発さ
- 停滞
- 明瞭さ
- おおむね明確
- 初心者へのやさしさ
- 30/100