Repository navigation
Open
Labels
Awaiting More FeedbackThis means we'd like to hear from more people who would be helped by this featureThis means we'd like to hear from more people who would be helped by this featureSuggestionAn idea for TypeScriptAn idea for TypeScript
Description
Suggestion
🔍 Search Terms
variant accessors index signatures record
✅ Viability 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. library functionality, non-ECMAScript syntax with JavaScript output, new syntax sugar for JS, etc.)
- This feature would agree with the rest of TypeScript's Design Goals.
⭐ Suggestion
Support for having Variant Accessors with index signature.
📃 Motivating Example
// Simple usage
interface NumberMap {
get [k: string](): number;
set [k: string](val: string | number);
}
const a: NumberMap = {}
a.test = '1';
a.test === 1;
// more complex
// makes number properties of an object to allow set `string`
type MakeStringAsNumber<T extends Record<string, any>> = {
get [ K in keyof T]: T[K]
set [ K in keyof T]: T[K] extends number ? T[K] | string : T[K]
}
declare const a: MakeStringAsNumber<{ a: boolean, b: number, c: string }>
a.b = '42'; // I want to have this avalible
a.c = '112'
// @ts-expect-error 'a' is boolean
a.a = '2'💻 Use Cases
The use case for this feature is vue 3 reactivity.
In Vue 3 there's Ref<T> (simplified as { value: T}) and Reactive<UnwrappedRef<T>> (unwraps {value: T} to T, recursive)
This feature would allow us to generate a type which can accept a Ref<T> | T
Basically:
const r = ref({ a: 1} )
r.value.a = 1
const a = reactive(r)
a.a // 1On a reactive/ref object the set in typescript must be UnwrappedRef<T> which is not true, because when you assign to a reactive it will unwrap the value:
const r = ref({a:1});
const a = ref({
r
}) // results in `{ r: { a: 1 } }`
a.r = r; // typescript error because `r` is `Ref<T>` instead of `UnwrappedRef<T>`, but it works at runtime.Reacted by Daniel Mittereder, Patrick Lienau, Alexandre Galays, whzx5byb, Mathis Bullinger, Alexander Turtsevich, Joe Calzaretta, Eduardo San Martin Morote, Kael, NN and 107 more
Reacted by ExE Boss, John lone and Samuel Braun
Reacted by wang, Moritz Rebbert, schelmo, Alfonso Andrés López Molina, reslear, Lachlan Miller, Erik Weitnauer, shmuelTopo, henribru, Craig Macomber (Microsoft) and 12 more
Reacted by ExE Boss, John lone, rrain0, Jerry and Samuel Braun
Metadata
Metadata
Assignees
Labels
Awaiting More FeedbackThis means we'd like to hear from more people who would be helped by this featureThis means we'd like to hear from more people who would be helped by this featureSuggestionAn idea for TypeScriptAn idea for TypeScript
Activity
rozzzly commentedon May 24, 2021
This would be very useful functionality which I'm saddened to see lacking. Here's my use case: I'm working on entity normalization package use with redux which allows for ORM-like CRUD functionality, relations, custom indexes, etc.
A contrived example would look something like:
I had hoped to be able to create a mapped type that would allow properties assignable to
Record(orRecord[], etc) be overwritten to allow this assignment by a different type:but there's no syntax to for anything like that. I thought perhaps something like how interfaces and types can have unnamed functions eg:
but
would just be interpreted as an object with two properties named
getandsetthat are both functions. Even though I only want to change the"settable"type of a subset of properties, Carlos Rodrigues (@pikax)'s suggestion could work for me without too much trouble.but alas, it's not possible right now. This functionality would greatly improve the usefulness of variant accessors. Right now I can manually create the setters/getters like so:
but that's rather tedious, and not nearly as extensible as a variant accessors for index signature. For example, if I wanted to create an index on
OwnerRecordwhere each key is the name of a dog and each property was aDogRecordthis would be trivial:but that's just not possible without variant accessors for index signatures. I'm sure there are many other developers who would love this functionality and I hope to see it implemented.
One interesting thing I noticed in Carlos Rodrigues (@pikax)'s examples is the lack of
()following the property name in the second example:not sure if this is unintentional, an alternative syntax, or implies some different behavior..
pikax commentedon May 24, 2021
yes that was not intentional 😅 Tbh I didn't put too much thought on the implementation, I can update the example showing a better API.
Other suggestion would be:
Having the special type
GetterSetterwould allow a finer control without a syntax change, because with{get, set}you are required to have the exact same keys on both and it might be a bit more tedious to have manage conditions across a fewget/setjustin-schroeder commentedon May 27, 2021
Would be hugely useful. I actually just opened an SO post today specifically around this issue for pretty much the same reason Carlos Rodrigues (@pikax) needs it in Vue 3 (although sort of inverse). Stack Overflow Here.
jcalz commentedon May 28, 2021
Should this issue be renamed to "variant accessors for index signatures and mapped types"?
rozzzly commentedon May 28, 2021
Carlos Rodrigues (@pikax)
I had the same thought about some magic built-in type such as
GetterSetter. This would certainly allow for much more precise control, but also would allow for something likeAssuming an unrestricted
GetterSetter, that second line would also be 'Foo' and—to me at least—that doesn't seem "right". While I would love the freedom toabuseuse this anywhere I please, I imagine this would be against a "fundamental tenet of type safety" or some such notion, not to mention probably causing a lot of issues with CFA and the like.Moreover, if you look at the built-in "utility types" (such as:
Exclude,Pick,ReturnType, etc) none of them really do any magic, they're just shortcuts over existing syntax and don't add any new behavior. For example,Northing special there, just a quick shortcut. To my knowledge all of the utility types are like that, with (perhaps) the exception of the template literal types like
Capitalize<...>So I would expect to see additional / altered syntax before a magic helper type like that. 🤷♂️AlexGalays commentedon Jun 3, 2021
I could use that too.
With a lib similar to
Immerthat added some extra methods to the wrapped types you typically would need to type a proxy'ed object like this:[-]Variant accessors for index signatures[/-][+]Variant accessors for index signatures and mapped types[/+]kemsky commentedon Aug 30, 2021
Here is my case:
Getter does not return undefined, setter accepts undefined:
Convert any type to accept undefined:
ccheraa commentedon Nov 6, 2021
Here's my use case:
I have a class
Data, I can use an instance of it to query data by calling a functiongetthat returns anotherDataobject.I can set data by calling
set.To read the data back, there's an async function
readthat returnsany.For example, I can do:
To type this, I wrapped it in a
Proxy:The previous code will become:
With variant accessors, I can change the
DatabaseProxydefinition to:And with some changes to the handler, my previous code would become:
thw0rted commentedon Nov 8, 2021
I don't mean to be overly critical, but your API reads like your first language is Ruby. Proxy abuse like that doesn't seem like the kind of thing TS should be encouraging. What's wrong with
database.get("admin.settings.defaultPage")? If you want to encode the database key hierarchy for type-checking, you can use conditional types to generate a union of template literal strings from an interface.ccheraa commentedon Nov 9, 2021
I agree with you, my approach only benefits the developer by offering typing and cleaner code, but that's not the issue here.
My issue is having a setter that accepts a value of type
Xwhile the getter returnsPromise<X>.PS: you said:
Is there a way to generate the type:
'admin.settings.defaultPage', and the others, from my previous interface?thw0rted commentedon Nov 9, 2021
There are a bunch of issues that address your "PS" question, but I haven't seen a consistent name for the problem. I usually search with keywords like "deep object path" or "deep keyof", which turns up this sort of thing. Using the updated answer from Joe Calzaretta (@jcalz) there, we get this result
Note that
type DBKeys = "admin" | "admin.settings" | "admin.settings.defaultPage". It's trickier, but still possible, to come up with a deep object path+value conditional type, such thatdatabase.get("admin.settings.defaultPage")returns astringbut you could have another path whose value is anumber, etc. If you really want to switch to that approach, I can try to dig up the implementation I'm using.ccheraa commentedon Nov 9, 2021
Thank you for your example, this helps make my code much better.
But I still believe that being able to get a Promise and set a value will make it even better.
2 remaining items
tree: better typed schema (#14243)
7nik commentedon Jul 23, 2023
I have a proxy that wraps values of properties but still allows assigning new values of the original type.
I thought I could type it properly because now, in 5.1, we can type getter and setter separately. But we still cannot use get/set on mapped types.
CraigMacomber commentedon Aug 10, 2023
After some testing, I'm also stuck on this. There are a lot of approaches that almost work, but I can't find one that actually works.
&actually produced a type which allowed all interaction either type allowed, we could make this work.settype when making a mapped type, it could work.writeonlythat generated setters, it could work.Sadly, I can't find any way to leverage this new expressiveness of setters with mapped types: they simply lose the setter type no matter what you do.
This might be fixable now that TypeScript can actually represent the types we want to generate as of 5.1, but it looks like we will need an extension to mapped types if we want be able to use it.
JesseRussell411 commentedon Oct 18, 2023
I have a slightly stupid use case for this.
You ever try to set a property on an object that might be null? Like, if it's null, then nothing should happen, but if it isn't, then the property should be set.
Like this.
foo?.bar = 7But you can't do that. I think it's a feature in dart, but this is javascript and the left hand side can't be optional.
So I wrote a little tool called nullset that looks like this
nullset(foo).bar = 7basically nullset returns foo if foo is not null (or undefined) but if it is, it just returns {}, which is a, sort-of, sacrificial object that will have a field set, but then just get garbage collected later, so nothing really happens.
And what's really neat is that you can use it with optional chains like this:
nullset(foo?.bar?.biv?.baz).bim = 7instead of doing this:
if (foo?.bar?.biv?.baz != null) foo?.bar?.biv?.baz.bim = 7But there are two problems with this tool. One is the sacrificial object that gets returned when the source object is null, which is a small performance concern. And the other one is typing, which is why I'm bringing this up. I can't figure out a safe way to type this. I essentially want to make the return type equal to the input type (nullset(): T) but write-only (nullset(): Writeonly). Making it read-only is easy, but there is no write-only as far as I know. I can't just return T (nullset(): T) because then typescript could think that {} was actually T and the user could read properities off of it that typescript says are not null (or undefined) but that are actually undefined.
This feature would let me do this.
but since I can't, in the meantime, I'm doing this.
nullset<T, F extends keyof T>(obj: T | null | undefined, field: F, value: T[F]): voidWhich, actually fixes the performance concern too by not creating the sacrificial object, but I think it's just... less cool.
nullset(foo?.bar?.biv?.baz, "bim", 7)vs
nullset(foo?.bar?.biv?.baz).bam = 7CraigMacomber commentedon Oct 18, 2023
My use case is that in Fluid Framework's new SharedTree, we provide a TypeScript powered schema system from which we generate APIs for the nodes. This allows us to generate a nice schema aware statically typed API for applications to use to work with their data, however we currently cannot support assignment to members of this tree in all cases due to this missing feature.
To be specific this code https://github.com/microsoft/FluidFramework/blob/0b5944050d3bc4470a87de4a4332235d37cb719c/experimental/dds/tree2/src/feature-libraries/editable-tree-2/editableTreeTypes.ts#L454-L466 implements the typing for our "struct" like nodes. Ideally it would generate getters and setters for each member based on the schema, but it is unable to do this. If we could replace
-readonlywith+writeonlyto generate setters, then inserting new content into the tree could be done in an ergonomic and type safe way.Our workaround for this is to provide factory functions that generate dummy node options which implement our whole node API (mostly with stubs) so you will be able to assign them, but if we could control the setter type correctly, we would not have to add all these stub methods that don't really work or make sense (ex: you can't get the parent or context of a node until its in the tree) and could instead make it properly type safe.
We really benefit from our schema system being directly in TypeScript: refactor rename, doc comments and go to definition on the struct fields just work, and take developers to the schema properly. This is really neat, and not something we could do practically with other approaches (for example doing ahead of time code generation of TypeScript files form schema). This assignment limitation is the one place we can't achieve the API we want this way.
Some more information about Fluid Framework and Shared tree can be found in: https://devblogs.microsoft.com/microsoft365dev/fluid-framework-2-0-alpha-is-now-available/
For details on SharedTrees schema aware API system, of which this is part can be found in https://github.com/microsoft/FluidFramework/tree/0b5944050d3bc4470a87de4a4332235d37cb719c/experimental/dds/tree2/src/feature-libraries/editable-tree-2
And some example usage of this API:
Right now, in that example, we can allow assigning to
part.namesince it's just a string. But if we replaced that with another struct, we would lose the ability to assign to it without introducing some kind of type unsafty unless we had this feature.Trakkasure commentedon Nov 29, 2023
In the case that you are presenting, is write-only a thing that is necessary?
A naive that I tried:
Assignment of z is improper use case and would be highlighted by many linters. If left unchanged it would set z to the number 3.
I'm sure I'm missing something in your explanation.
nex3 commentedon Oct 9, 2024
My use-case for this is pretty straightforward: I'm exposing an AST API with a
Proxyover aRecord<string, Expression>and I want to allow users to setstrings that get automatically parsed into expressions, for examplenode.configuration.foo = "1 + 2". This works fine in plain JS, but is currently untypeable in TypeScript.Trakkasure commentedon Oct 10, 2024
can you expand on this with a simplified example. I see what is in quotes, but it is not enough to wrap my head around to understand what you mean.
Upgrade to TS5.6.
Upgrade to TS 5.6.
feat: Ergonomics overhaul. Camel-case, TS types, codegen + more.
feat: Ergonomics overhaul. Camel-case, TS types, codegen + more.
feat: Ergonomics overhaul. Camel-case, TS types, codegen + more.
hamzakubba commentedon Jun 1, 2025
Hopefully better late than never, here is an example (also on TS playground):
This example very roughly matches what I am trying to accomplish, for an ECS library where there's a function that dynamically generates a class based on some config. I've changed the structure a bunch from what it actually is, to demonstrate the problem more clearly, so I'm not looking for alternatives to the above structure because they wouldn't help in my actual situation, but having the desired solution at the end (or an equivalent) would solve the issue. Thank you!
fix(dts): accessors emit as accessors — with asymmetric read/write types