Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Consider adding symbolof type operator, like keyof but for unique symbol properties #20721

Closed
yortus opened this issue Dec 15, 2017 · 19 comments · Fixed by #23592
Closed

Consider adding symbolof type operator, like keyof but for unique symbol properties #20721

yortus opened this issue Dec 15, 2017 · 19 comments · Fixed by #23592
Labels
Fixed A PR has been merged for this issue Suggestion An idea for TypeScript

Comments

@yortus
Copy link
Contributor

yortus commented Dec 15, 2017

EDIT: The example code below mostly does work as per #20721 (comment). The only part remaining unsolved is that there is no equivelent of keyof for unique symbols. The suggestion is to add a symbolof type 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

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[STR];     // ERROR: Cannot find name 'STR'
type T3 = Foo[SYM];     // ERROR: Cannot find name 'SYM'

// index type query
type K = keyof Foo;     // K = 'A string' | 'lit'      (but no SYM)

Expected behavior:
No errors. T2 is boolean and T3 is number. keyof Foo is 'A string' | 'lit' | SYM

Actual behavior:
Errors for T2 and T3, and keyof Foo doesn't include SYM.

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 keyof queries is by design, but what about the fact that we can't query types with Foo[STR] or Foo[SYM], even though the compiler knows the types and the syntax is consistent with Foo['lit']?

@dinoboff
Copy link

I hope keyof will 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.

@DanielRosenwasser DanielRosenwasser added the Bug A bug in TypeScript label Dec 19, 2017
@DanielRosenwasser DanielRosenwasser added this to the TypeScript 2.7 milestone Dec 19, 2017
@weswigham
Copy link
Member

weswigham commented Jan 10, 2018

@yortus Indexes work, you're just accessing the type of a const incorrectly:

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 keyof return symbols.... Object.keys only returns string keys at runtime, and many other JS constructs only operate over string keys. I think we may do symbolof operator to maintain compatibility and keep a distinction between the two namespaces.

@yortus
Copy link
Contributor Author

yortus commented Jan 10, 2018

Thanks @weswigham. So unique symbol values (and const strings) do not create same-named unit types. And this is by design as explained by @mhegazy in #20898 (comment).

I think symbolof would be a good addition if it means unique symbol keys can play a part in mapped types and other type inference scenarios.

@sandersn sandersn added Suggestion An idea for TypeScript and removed Bug A bug in TypeScript labels Jan 10, 2018
@sandersn sandersn removed this from the TypeScript 2.7.1 milestone Jan 10, 2018
@yortus yortus 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 Feb 15, 2018
@yortus
Copy link
Contributor Author

yortus commented Feb 15, 2018

I updated the title and description to more clearly reflect what is being suggested here.

@robbiespeed
Copy link

Here's an example of where a symbolof operator 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 symbolof we 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);
// symbol

Using 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

@ghost
Copy link

ghost commented Feb 16, 2018

I've added significant justification for keyof supporting both symbol and string properties in the "other issue" (I didn't see the significance when I checked for duplicates).

Those watching this issue may be interested in that, apologies for the dupe.

#21983

@ghost
Copy link

ghost commented Feb 16, 2018

Sorry, me again. More specifically the justification (which I think is fairly concrete) starts at this comment: #21983 (comment)

@kpdonn
Copy link
Contributor

kpdonn commented Mar 30, 2018

I'm very in favor of at least a symbolof operator, although personally I'm even more in favor of just having keyof return symbols as well. Having a separate operator in cases when you want to operate on both would be pretty cumbersome and I imagine for that reason a lot of types would continue to be written only with keyof even if at runtime they'd have no problem handling symbols as well.

In the hypothetical case where you really need specifically string keys you'd presumably get a compile error somewhere from typescript when you try to pass or assign a string | symbol to a spot that requires a string. And then you could rewrite keyof T to be string & keyof T to fix the error.

Is it actually very common to require specifically strings though when you have a keyof T type? The only specific case I saw mentioned was the return type of Object.keys, but that doesn't actually return a keyof T in typescript. It just returns string[] because typescript can't be sure that there won't be extra fields on T at runtime that it doesn't know about.

Edit: Also notable is the fact that keyof T initially had a type of string | number when it was first added but was changed to be only string shortly afterwards in #12425 with the reason:

This more accurately reflects how properties work in JavaScript and allows us to use keyof T as the inferred type of a for...in variable when the object is of a type parameter type.

To me that pretty clearly shows that accurately reflecting how properties work in JavaScript is a goal of keyof, and part of that is that symbols can be properties. As MDN puts it:

A symbol value may be used as an identifier for object properties; this is the data type's only purpose.

It seems to me to be a fairly big hole in typescript to go through the trouble of adding the symbol type but then not include them in the keyof operator, when being a key of an object is their only purpose.

Admittedly the part about for...in being part of the motivation for removing number from keyof does not help the argument for symbols to be included since they are never included in for...in either, but it seems like it wouldn't be that hard to handle that specific case specially.

@ghost
Copy link

ghost commented Mar 30, 2018 via email

@kpdonn
Copy link
Contributor

kpdonn commented Mar 30, 2018

Uh so fun fact I just learned: it turns out keyof T does in fact return any unique symbol on T as of 2.8??? I'm not sure if I just missed something or if this is unintentional or what but take for instance this code from earlier in this issue that didn't previously work:

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"'.~~
// Not anymore in TS 2.8 

type KeyofObj = keyof typeof obj
// typeof sym | 'str' | 'num' in 2.8

type Values<T> = T[keyof T]

type ValuesOfObj = Values<typeof obj>
// string | number | symbol in 2.8

Playground link

I just confirmed that that didn't previously work in 2.7. As far as I can tell the only thing we are actually missing now is the ability to use symbols in mapped types. If it was really a breaking change to make keyof return symbols apparently it was quite a small one because it didn't even make it onto the breaking changes page and glancing at recent issues I didn't notice anything started about it.

@yortus
Copy link
Contributor Author

yortus commented Mar 30, 2018

That is weird. I wonder if this change to keyof behaviour was intentional.

The change doesn't seem in line with @weswigham's comment above, and as you point out breaking changes would normally be documented.

@kpdonn
Copy link
Contributor

kpdonn commented Mar 30, 2018

@weswigham It looks like you ended up changing keyof to 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.

@weswigham
Copy link
Member

You shouldn't rely on keyof returning numbers and unique symbols - it's definitely a bug because a keyof T is a subtype of string, and changing that would likely break a lot. We're going to look into fixing keyof'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 with propkeyof T, but are open to suggestions).

@kpdonn
Copy link
Contributor

kpdonn commented Apr 3, 2018

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. Any (edit: Some) code that for some reason actually requires keyof to always be a string is 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

Playground link

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 propof over propkeyof so that it at least has a fighting chance of being used regularly instead of the shorter and already common keyof.

Edit: Clarified that I was wrong when I thought any potential breaking changes would've already been happening in 2.8.

@weswigham
Copy link
Member

weswigham commented Apr 3, 2018

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 keyof on concrete types, methinks). If keyof can be a symbol or number, it should not, and that inconsistency is what's really a bug.

@kpdonn
Copy link
Contributor

kpdonn commented Apr 3, 2018

Fair enough I wasn't thinking of that. I still question how common that use case(trying to assign keyof T to a string for some reason) really is. Anywhere I'm declaring something to be a keyof T I'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 propkeyof or propof operator is added how common will it be to still want plain keyof? The most basic use case for keyof is 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 T because 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 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.

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);
}

@yortus
Copy link
Contributor Author

yortus commented Apr 3, 2018

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.

@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 & string if you want just the string keys.

@ahejlsberg
Copy link
Member

With #23592 keyof supports numeric literals and unique symbols.

@robbiespeed
Copy link

I was worried about keyof including symbols, because narrowing with & string returns 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 Extract you can easily write Extract<keyof T, string> to get a return type of only the string keys.

@mhegazy mhegazy added this to the TypeScript 2.9 milestone Apr 23, 2018
@mhegazy mhegazy added Fixed A PR has been merged for this issue and removed In Discussion Not yet reached consensus labels Apr 23, 2018
@microsoft microsoft locked and limited conversation to collaborators Jul 31, 2018
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
Fixed A PR has been merged for this issue Suggestion An idea for TypeScript
Projects
None yet
Development

Successfully merging a pull request may close this issue.

9 participants