Skip to content

Fix multiple issues with indexed access types applied to mapped types - #47109

Merged
ahejlsberg merged 2 commits into
mainfrom
fix30581
Dec 14, 2021
Merged

Fix multiple issues with indexed access types applied to mapped types#47109
ahejlsberg merged 2 commits into
mainfrom
fix30581

Conversation

@ahejlsberg

@ahejlsbergahejlsberg commented Dec 10, 2021

Copy link
Copy Markdown
Member

This PR fixes multiple issues related to indexed access types applied to mapped types. The fixes enable most of the problems outlined in #30581 to be solved using an approach that involves "distributive object types" created using indexed access types applied to mapped types.

First, a summary of the issue we're trying to solve:

typeUnionRecord=|{kind: "n",v: number,f: (v: number)=>void}|{kind: "s",v: string,f: (v: string)=>void}|{kind: "b",v: boolean,f: (v: boolean)=>void};functionprocessRecord(rec: UnionRecord){rec.f(rec.v);// Error, 'string | number | boolean' not assignable to 'never'}

The UnionRecord type is a union of records with two correlated properties, v and f, where the type of v is always the same as the type of f's parameter. In the processRecord function we're attempting to take advantage of the correlation by calling rec.f(rec.v) for some record. By casual inspection this seems perfectly safe, but the type checker doesn't see it that way. From its viewpoint, the type of rec.v is string | number | boolean and the type of rec.f is (v: never) => void, the never originating from the intersection string & number & boolean. The type is basically trying to check if a v from some record can be passed as an argument to an f from some record--but not necessarily the same record.

In the following I will outline an approach to writing types for this kind of pattern. Key to the approach is that the union types in question contain a discriminant property with a type that can itself serve as a property name (e.g. a string literal type or a unique symbol type). Note that the examples below only compile successfully with the fixes in this PR.

The following formulation of the UnionRecord type encodes the correspondence between kinds and types and the correlated properties:

typeRecordMap={n: number,s: string,b: boolean};typeRecordType<KextendskeyofRecordMap>={kind: K,v: RecordMap[K],f: (v: RecordMap[K])=>void};typeUnionRecord=RecordType<'n'>|RecordType<'s'>|RecordType<'b'>;

The manual step of creating a union of RecordType<K> for each key in RecordMap can instead be automated:

typeUnionRecord={[PinkeyofRecordMap]: RecordType<P>}[keyofRecordMap];

The pattern of applying an indexed access type to a mapped type is effectively a device for distributing a type (in this case RecordType<P>) over a union (in this case keyof RecordMap). We can even go one step further and allow a UnionRecord to be created for any arbitrary subset of keys:

typeUnionRecord<KextendskeyofRecordMap=keyofRecordMap>={[PinK]: RecordType<P>}[K];

We can then merge RecordType<K> into UnionRecord<K>, which removes the possibility of creating non-distributed records. This leaves us with the final formulation:

typeRecordMap={n: number,s: string,b: boolean};typeUnionRecord<KextendskeyofRecordMap=keyofRecordMap>={[PinK]: {kind: P,v: RecordMap[P],f: (v: RecordMap[P])=>void}}[K];

And, using this formulation, we can now write types for the processRecord function that reflect the correlation and work for singletons as well as unions:

functionprocessRecord<KextendskeyofRecordMap>(rec: UnionRecord<K>){rec.f(rec.v);// Ok}declareconstr1: UnionRecord<'n'>;// { kind: 'n', v: number, f: (v: number) => void }declareconstr2: UnionRecord;// { kind: 'n', ... } | { kind: 's', ... } | { kind: 'b', ... }processRecord(r1);processRecord(r2);processRecord({kind: 'n',v: 42,f: v=>v.toExponential()});

And, because everything is expressed in terms of the mapping in RecordMap, new kinds and data types can be added in a single place, nicely conforming to the DRY principle.

Fixes#30581.

@typescript-bottypescript-bot added Author: Team For Uncommitted Bug PR for untriaged, rejected, closed or missing bug labels Dec 10, 2021
@ahejlsberg

Copy link
Copy Markdown
MemberAuthor

@typescript-bot perf test faster
@typescript-bot test this
@typescript-bot user test inline
@typescript-bot run dt

@typescript-bot

typescript-bot commented Dec 10, 2021

Copy link
Copy Markdown
Contributor

Heya @ahejlsberg, I've started to run the abridged perf test suite on this PR at d42e4c2. You can monitor the build here.

Update: The results are in!

@typescript-bot

typescript-bot commented Dec 10, 2021

Copy link
Copy Markdown
Contributor

Heya @ahejlsberg, I've started to run the extended test suite on this PR at d42e4c2. You can monitor the build here.

@typescript-bot

typescript-bot commented Dec 10, 2021

Copy link
Copy Markdown
Contributor

Heya @ahejlsberg, I've started to run the parallelized Definitely Typed test suite on this PR at d42e4c2. You can monitor the build here.

@ahejlsberg

Copy link
Copy Markdown
MemberAuthor

@typescript-bot user test this inline

@typescript-bot

typescript-bot commented Dec 10, 2021

Copy link
Copy Markdown
Contributor

Heya @ahejlsberg, I've started to run the inline community code test suite on this PR at d42e4c2. You can monitor the build here.

Update: The results are in!

@typescript-bot

Copy link
Copy Markdown
Contributor

@ahejlsberg
The results of the perf run you requested are in!

Here they are:

Comparison Report - main..47109

Metricmain47109DeltaBestWorst
Angular - node (v14.15.1, x64)
Memory used331,241k (± 0.01%)331,232k (± 0.01%)-9k (- 0.00%)331,147k331,307k
Parse Time1.95s (± 0.46%)1.94s (± 1.45%)-0.01s (- 0.67%)1.92s2.05s
Bind Time0.86s (± 0.64%)0.86s (± 0.52%)-0.00s (- 0.46%)0.85s0.87s
Check Time5.41s (± 0.36%)5.40s (± 0.40%)-0.01s (- 0.15%)5.37s5.47s
Emit Time6.21s (± 0.65%)6.17s (± 0.65%)-0.04s (- 0.69%)6.11s6.28s
Total Time14.44s (± 0.34%)14.38s (± 0.30%)-0.06s (- 0.42%)14.28s14.46s
Compiler-Unions - node (v14.15.1, x64)
Memory used191,776k (± 0.60%)191,902k (± 0.62%)+126k (+ 0.07%)190,235k193,521k
Parse Time0.81s (± 1.00%)0.81s (± 0.94%)-0.01s (- 0.86%)0.79s0.83s
Bind Time0.56s (± 0.59%)0.56s (± 0.65%)-0.00s (- 0.71%)0.55s0.56s
Check Time7.44s (± 0.76%)7.48s (± 0.77%)+0.04s (+ 0.52%)7.36s7.66s
Emit Time2.48s (± 0.75%)2.48s (± 0.84%)-0.00s (- 0.08%)2.44s2.54s
Total Time11.29s (± 0.51%)11.32s (± 0.60%)+0.03s (+ 0.25%)11.19s11.50s
Monaco - node (v14.15.1, x64)
Memory used324,384k (± 0.00%)324,393k (± 0.01%)+9k (+ 0.00%)324,357k324,431k
Parse Time1.51s (± 0.62%)1.52s (± 1.26%)+0.00s (+ 0.20%)1.49s1.59s
Bind Time0.76s (± 0.90%)0.76s (± 0.48%)0.00s ( 0.00%)0.75s0.76s
Check Time5.33s (± 0.61%)5.31s (± 0.52%)-0.02s (- 0.32%)5.27s5.40s
Emit Time3.26s (± 0.46%)3.25s (± 0.66%)-0.01s (- 0.37%)3.21s3.32s
Total Time10.86s (± 0.44%)10.83s (± 0.44%)-0.02s (- 0.21%)10.73s10.95s
TFS - node (v14.15.1, x64)
Memory used289,204k (± 0.01%)289,192k (± 0.01%)-12k (- 0.00%)289,155k289,236k
Parse Time1.24s (± 0.72%)1.24s (± 0.89%)+0.00s (+ 0.32%)1.22s1.28s
Bind Time0.73s (± 0.41%)0.73s (± 0.76%)-0.00s (- 0.00%)0.72s0.75s
Check Time4.95s (± 0.45%)4.96s (± 0.57%)+0.00s (+ 0.10%)4.90s5.03s
Emit Time3.50s (± 1.22%)3.49s (± 0.63%)-0.02s (- 0.49%)3.44s3.55s
Total Time10.43s (± 0.48%)10.41s (± 0.43%)-0.01s (- 0.13%)10.30s10.49s
material-ui - node (v14.15.1, x64)
Memory used448,288k (± 0.06%)448,440k (± 0.00%)+152k (+ 0.03%)448,386k448,488k
Parse Time1.84s (± 0.36%)1.85s (± 2.15%)+0.00s (+ 0.27%)1.80s2.00s
Bind Time0.68s (± 0.65%)0.68s (± 0.76%)-0.00s (- 0.15%)0.67s0.70s
Check Time12.96s (± 0.97%)12.87s (± 0.63%)-0.09s (- 0.70%)12.76s13.11s
Emit Time0.00s (± 0.00%)0.00s (± 0.00%)0.00s ( NaN%)0.00s0.00s
Total Time15.49s (± 0.81%)15.40s (± 0.60%)-0.09s (- 0.59%)15.27s15.62s
xstate - node (v14.15.1, x64)
Memory used532,835k (± 0.00%)532,865k (± 0.00%)+30k (+ 0.01%)532,843k532,913k
Parse Time2.55s (± 0.23%)2.56s (± 0.56%)+0.01s (+ 0.51%)2.53s2.59s
Bind Time1.15s (± 0.65%)1.16s (± 0.91%)+0.00s (+ 0.35%)1.13s1.18s
Check Time1.48s (± 0.54%)1.48s (± 0.87%)-0.00s (- 0.13%)1.46s1.52s
Emit Time0.07s (± 0.00%)0.07s (± 0.00%)0.00s ( 0.00%)0.07s0.07s
Total Time5.26s (± 0.24%)5.27s (± 0.42%)+0.01s (+ 0.19%)5.22s5.32s
System
Machine Namets-ci-ubuntu
Platformlinux 4.4.0-210-generic
Architecturex64
Available Memory16 GB
Available Memory8 GB
CPUs4 × Intel(R) Core(TM) i7-4770 CPU @ 3.40GHz
Hosts
  • node (v14.15.1, x64)
Scenarios
  • Angular - node (v14.15.1, x64)
  • Compiler-Unions - node (v14.15.1, x64)
  • Monaco - node (v14.15.1, x64)
  • TFS - node (v14.15.1, x64)
  • material-ui - node (v14.15.1, x64)
  • xstate - node (v14.15.1, x64)
BenchmarkNameIterations
Current4710910
Baselinemain10

Developer Information:

Download Benchmark

@typescript-bot

Copy link
Copy Markdown
Contributor

@ahejlsberg
Great news! no new errors were found between main..refs/pull/47109/merge

@jcalz

Copy link
Copy Markdown
Contributor

I'm kind of surprised at how much of this already works in TS4.5 without these fixes. Both processRecord(r1); and processRecord(r2); are fine. Sure, the K parameter isn't inferred, but it falls back to keyof RecordMap which is fine for a distributive object type. Only

processRecord({kind: 'n',v: 42,f: v=>v.toExponential()});// error

fails outright, and that's just because of unfortunate contextual typing of v as RecordMap[keyof RecordMap] . In current TS I'd be inclined to fix that with either of these:

processRecord<'n'>({kind: 'n',v: 42,f: v=>v.toExponential()});// okayprocessRecord({kind: 'n',v: 42,f: (v: number)=>v.toExponential()});// okay

Am I right in thinking the current fix is mostly helping inference for K succeed here so that these are not necessary? I'm definitely interested in playing around with this. Thanks for working on it!!

Playground link

@ahejlsberg

ahejlsberg commented Dec 11, 2021

Copy link
Copy Markdown
MemberAuthor

I'm kind of surprised at how much of this already works in TS4.5 without these fixes.

Yes, the nice thing is that this approach isn't really a new feature. The core capabilities were all there, they just didn't work quite right in a few cases. This in contrast to a whole new feature like existential types, which would be a very complex undertaking.

Am I right in thinking the current fix is mostly helping inference for K succeed here so that these are not necessary?

The PR has three fixes in it. Two of them relate to inference, specifically that we would fail to infer from object literals that contain contextually typed arrow functions.

The third fix relates to indexing non-generic mapped types with generic index types. For example:

typeRecordFuncMap={[PinkeyofRecordMap]: (x: RecordMap[P])=>void};constrecordFuncs: RecordFuncMap={n: n=>n.toFixed(2),s: s=>s.length,b: b=>b ? 1 : 0,}functioncallRecordFunc<KextendskeyofRecordMap>(rec: UnionRecord<K>){constfn=recordFuncs[rec.kind];// RecordFuncMap[K]fn(rec.v);// Would fail, but works with this PR}

The call in callRecordFunc failed to compile because we transformed type RecordFuncMap[K] into RecordFuncMap[typeof RecordMap] by constraint substitution when obtaining what we call the apparent type of fn. With the fix in the PR, we instead transform RecordFuncMap[K] into (x: RecordMap[K]) => void.

There are more examples of this pattern in the tests I just added to the PR. See here.

@ahejlsberg

Copy link
Copy Markdown
MemberAuthor

Performance is unaffected by this PR and all tests look clean. I think this is ready to merge.

@ahejlsberg
ahejlsberg merged commit 3d3825e into mainDec 14, 2021
@ahejlsberg
ahejlsberg deleted the fix30581 branch December 14, 2021 19:51
zerobias added a commit to effector/effector that referenced this pull request Dec 14, 2021
Anders Hejlsberg fixed issues of version 4.5.4 in microsoft/TypeScript#47109 so I revert typescript version until it will be released
Hejlsberg saved my day ❤️
@quixot1c

Copy link
Copy Markdown

I have run into an oddity that might be related to this PR. Consider the following code:

enumMyEvent{One,Two}typeCallback<T>=(arg: T)=>void;interfaceCallbackTypes{[MyEvent.One]: number[MyEvent.Two]: string}typeCallbacks={[EinMyEvent]: Callback<CallbackTypes[E]>[]};classCallbackContainer{callbacks: Callbacks={[MyEvent.One]: [],[MyEvent.Two]: []};constructor(){}setListener<EextendsMyEvent>(event: E,callback: Callback<CallbackTypes[E]>){constcallbacks=this.callbacks[event];callbacks.push(callback);// okay}getListeners<EextendsMyEvent>(event: E){returnthis.callbacks[event];}}

Thanks to this PR above code now works! But when I change const callbacks = this.callbacks[event] to const callbacks: Array<Callback<CallbackTypes[E]>> = this.callbacks[event] the compiler throws Type 'Callbacks[E]' is not assignable to type 'Callback<CallbackTypes[E]>[]'. But when (in both versions) examining the type of callbacks.push, the Array is typed as Array<Callback<CallbackTypes[E]>>.

Why then, is Callbacks[E] not assignable to Callback<CallbackTypes[E]>[] while in the push call the Callbacks[E] is automagically converted to that very same type Array<Callback<CallbackTypes[E]>>?

@jfet97jfet97 mentioned this pull request Jun 29, 2023
@jcalzjcalz mentioned this pull request Jul 5, 2024
6 tasks
@Andarist

Copy link
Copy Markdown
Contributor

This PR is linked from TypeScript 4.6 release notes but the implementation changed soon after it here: #47370

@jcalzjcalz mentioned this pull request Jan 24, 2025
6 tasks
@microsoftmicrosoft locked as resolved and limited conversation to collaborators Oct 16, 2025
Sign up for freeto subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

Author: TeamFor Uncommitted BugPR for untriaged, rejected, closed or missing bug

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Wishlist: support for correlated union types

6 participants

@ahejlsberg@typescript-bot@jcalz@quixot1c@Andarist@RyanCavanaugh