Skip to content

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

Description

@yortus

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']?

Activity

  1. dinoboff commented on Dec 17, 2017

    @dinoboff

    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.

  2. weswigham commented on Jan 10, 2018

    @weswigham
    Member

    Troy Gerwien (@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.

  3. yortus commented on Jan 10, 2018

    @yortus
    ContributorAuthor

    Thanks 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 symbolof would be a good addition if it means unique symbol keys can play a part in mapped types and other type inference scenarios.

  4. 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
  5. yortus commented on Feb 15, 2018

    @yortus
    ContributorAuthor

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

  6. robbiespeed commented on Feb 15, 2018

    @robbiespeed

    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
    
  7. 5 remaining items

  8. yortus commented on Mar 30, 2018

    @yortus
    ContributorAuthor

    That is weird. I wonder if this change to keyof behaviour 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.

  9. kpdonn commented on Mar 30, 2018

    @kpdonn
    Contributor

    Wesley Wigham (@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.

  10. weswigham commented on Apr 2, 2018

    @weswigham
    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).

  11. kpdonn commented on Apr 3, 2018

    @kpdonn
    Contributor

    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.

  12. weswigham commented on Apr 3, 2018

    @weswigham
    Member

    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.

  13. kpdonn commented on Apr 3, 2018

    @kpdonn
    Contributor

    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);
    }
  14. yortus commented on Apr 3, 2018

    @yortus
    ContributorAuthor

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

  15. ahejlsberg commented on Apr 20, 2018

    @ahejlsberg
    Member

    With #23592 keyof supports numeric literals and unique symbols.

  16. robbiespeed commented on Apr 21, 2018

    @robbiespeed

    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.

  17. added
    FixedA PR has been merged for this issue
    and removed on Apr 23, 2018
  18. locked and limited conversation to collaborators on Jul 31, 2018
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    FixedA PR has been merged for this issueSuggestionAn idea for TypeScript

    Type

    No type

    Projects

    No projects

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions