Repository navigation
Consider adding symbolof type operator, like keyof but for unique symbol properties #20721
Description
Activity
I hope
keyofwill support symbols. Symbol keys are not enumerable but they are not hidden; you should be able to test a symbol is a key of an object.- addedBugA bug in TypeScriptA bug in TypeScript
on Dec 19, 2017 Troy Gerwien (@yortus) Indexes work, you're just accessing the type of a
constincorrectly:const SYM = Symbol('A Symbol'); const STR = 'A String'; interface Foo { 'lit': string; [STR]: boolean; [SYM]: number; } declare let foo: Foo; let v1 = foo['lit']; // v1 is string let v2 = foo[STR]; // v2 is boolean let v3 = foo[SYM]; // v3 is number // indexed access types type T1 = Foo['lit']; // T1 = string type T2 = Foo[typeof STR]; // T2 = boolean (note `typeof`) type T3 = Foo[typeof SYM]; // T3 = number (note `typeof`)
As for making
keyofreturn symbols....Object.keysonly returns string keys at runtime, and many other JS constructs only operate over string keys. I think we may dosymbolofoperator to maintain compatibility and keep a distinction between the two namespaces.Reacted by Nathan Shively-Sanders, Robbie Speed and Aluan HaddadThanks Wesley Wigham (@weswigham). So unique symbol values (and const strings) do not create same-named unit types. And this is by design as explained by Mohamed Hegazy (@mhegazy) in #20898 (comment).
I think
symbolofwould be a good addition if it means unique symbol keys can play a part in mapped types and other type inference scenarios.Reacted by Patrick Lienau- addedSuggestionAn idea for TypeScriptAn idea for TypeScriptand removedBugA bug in TypeScriptA bug in TypeScript
on Jan 10, 2018 - changed the title
[-]Can't use unique symbols or const strings in indexed access types or index type queries[/-][+]Consider adding `symbolof` type operator, like `keyof` but for unique symbol properties[/+]on Feb 15, 2018 I updated the title and description to more clearly reflect what is being suggested here.
Here's an example of where a
symbolofoperator could be used.const sym = Symbol(); const obj = { num: 0, str: 's', [sym]: sym }; function set <T extends object, K extends keyof T> (obj: T, key: K, value: T[K]): T[K] { return obj[key] = value; } const val = set(obj, 'str', ''); // string const valB = set(obj, 'num', ''); // Expect type error // Argument of type '""' is not assignable to parameter of type 'number'. const valC = set(obj, sym, sym); // Unexpected type error // Argument of type 'unique symbol' is not assignable to parameter of type '"str" | "num"'.If we had a
symbolofwe could do:function set <T extends object, K extends keyof T | symbolof T> (obj: T, key: K, value: T[K]): T[K] { return obj[key] = value; } const val = set(obj, 'str', ''); // string const valB = set(obj, 'num', ''); // Expect type error // Argument of type '""' is not assignable to parameter of type 'number'. const valC = set(obj, sym, sym); // symbolUsing overloads we can work around the issue somewhat, but we still have no type checking of symbol properties.
function set (obj: object, key: symbol, value: any): any function set <T extends object, K extends keyof T> (obj: T, key: K, value: T[K]): T[K]; function set (obj, key, value) { return obj[key] = value; } const val = set(obj, 'str', ''); // string const valB = set(obj, 'num', ''); // Expect type error // Argument of type '""' is not assignable to parameter of type 'number'. const valC = set(obj, sym, sym); // any- addedIn DiscussionNot yet reached consensusNot yet reached consensus
on Feb 15, 2018 5 remaining items
That is weird. I wonder if this change to
keyofbehaviour was intentional.The change doesn't seem in line with Wesley Wigham (@weswigham)'s comment above, and as you point out breaking changes would normally be documented.
Wesley Wigham (@weswigham) It looks like you ended up changing
keyofto return unique symbols in #22339? I tested before and after that PR and that's what changed the behavior. Wanted to point it out since I'm guessing it wasn't intentional based on your comments earlier in this issue and the fact that there is no mention of it in the PR.You shouldn't rely on
keyofreturningnumbers andunique symbols - it's definitely a bug because akeyof Tis a subtype ofstring, and changing that would likely break a lot. We're going to look into fixingkeyof's behavior (meaning it should omit symbols and map numbers to their string representation) while also adding a new operator that can correctly retrieve the declared keys of an object (we're toying withpropkeyof T, but are open to suggestions).Reacted by Damien Lebrun and Bao BoForgive me for being blunt, but I don't see how you can say changing it would likely break a lot when you already did change it to no longer always only be a subtype of string and it didn't break enough for anyone to even notice.
Any(edit: Some) code that for some reason actually requireskeyofto always be astringis already breaking in 2.8:const sym = Symbol() const obj = { num: 0, str: 's', [sym]: true } declare const objKeys: keyof typeof obj const strObjKeys: string = objKeys // error unique symbol is not assignable to string in 2.8
So
whatever(Edit: some) damage there would be is already being done, yet as far as I can tell there have been zero issues reported against it almost a month after it was merged and a week after 2.8 was released. That makes it hard for me to believe it's really that problematic of a breaking change.If you do have to change it back though and add another operator instead, I'd prefer
propofoverpropkeyofso that it at least has a fighting chance of being used regularly instead of the shorter and already commonkeyof.Edit: Clarified that I was wrong when I thought any potential breaking changes would've already been happening in 2.8.
Forgive me for being blunt, but I don't see how you can say changing it would likely break a lot when you already did change it to no longer always only be a subtype of string and it didn't break enough for anyone to even notice
That's because
declare function log(x: string): void; function f<T>(x: T, k: keyof T) { log(k); }
still works (not may people have reason to call
keyofon concrete types, methinks). Ifkeyofcan be a symbol or number, it should not, and that inconsistency is what's really a bug.Fair enough I wasn't thinking of that. I still question how common that use case(trying to assign
keyof Tto astringfor some reason) really is. Anywhere I'm declaring something to be akeyof TI'm doing it because I want to be able to do an indexed access with it and for that use case a symbol works just as well as a string.If a
propkeyoforpropofoperator is added how common will it be to still want plainkeyof? The most basic use case forkeyofis something like:function getPropValue<T, K extends keyof T>(obj: T, key: K): T[K] { return obj[key] }
Which would be much better typed with
K extends propkeyof Tbecause symbols will work with that code just as well as strings.I think that we'd end up in an unfortunate situation where accurate types should normally be using
propkeyofbut it'll end up being far more common for people to just usekeyofbecause of inertia and being slightly shorter to type.Not the end of the world but it'd be nice to avoid. Especially since the workaround for the (still hypothetical to me) cases where it'd be a breaking change seems to be as simple as:
declare function log(x: string): void; function f<T>(x: T, k: string & keyof T) { log(k); }
I think that we'd end up in an unfortunate situation where accurate types should normally be using propkeyof but it'll end up being far more common for people to just use keyof because of inertia and being slightly shorter to type.
Kevin Donnelly (@kpdonn) I think you make a valid point there. There's already a precedent for that happening with
any.I like your suggestion to have a single general operator (
keyof), and narrow it with& stringif you want just the string keys.With #23592
keyofsupports numeric literals and unique symbols.Reacted by Kevin Donnelly, Robbie Speed, Wesley Wigham, Damien Lebrun, SlurpTheo and Bao BoI was worried about
keyofincluding symbols, because narrowing with& stringreturns some really awful types.(typeof sym & string) | ("a" & string) | ("b" & string)in the simple instance of only three keys.However with the introduction of condition types and
Extractyou can easily writeExtract<keyof T, string>to get a return type of only the string keys.- addedFixedA PR has been merged for this issueA PR has been merged for this issueand removedIn DiscussionNot yet reached consensusNot yet reached consensus
on Apr 23, 2018 - locked and limited conversation to collaborators
on Jul 31, 2018
EDIT: The example code below mostly does work as per #20721 (comment). The only part remaining unsolved is that there is no equivelent of
keyoffor unique symbols. The suggestion is to add asymboloftype operator.TypeScript Version: 2.7.0-dev.20171215
Thanks to #15473, unique symbols and string consts can be used as keys in types. But they don't work with indexed access types or index type queries (#11929), as demonstated below.
Code
Expected behavior:
No errors.
T2isbooleanandT3is number.keyof Foois'A string' | 'lit' | SYMActual behavior:
Errors for
T2andT3, andkeyof Foodoesn't includeSYM.I know there are several overlapping features in play here, but would like to know if all of the actual behaviour above is by design. E.g., maybe not having symbols show up in
keyofqueries is by design, but what about the fact that we can't query types withFoo[STR]orFoo[SYM], even though the compiler knows the types and the syntax is consistent withFoo['lit']?