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

Keysof type operator #10425

Closed
wants to merge 5 commits into from
Closed

Conversation

weswigham
Copy link
Member

Fixes half of #1295. (Honestly, that issue is like proposals for three separate but interoperating features) The remaining parts of #1295 are what I call a "type access expression" - a mirror of element access expressions, but for types (quite literally the same logic) - and altering index resolution logic to, rather than using unions to check for index signature compatibility, be able to use a union of string literals to create a union of the types of those members.

The part that this PR addresses is an operator for finding all of the keys of an arbitrary type. This PR adds a new "type operator" (akin to typeof) - keysof. Used like so:

interface Item<T> {
  owner: Person;
  id: number;
  contents: T
}

let x: keysof Item<any>;
x = "id"; // valid
x = "owner" // valid
x = "content" // error! "content" not assignable to keysof Item<any>

interface HelloWorld { hello: any; world: any; }

function check<K extends keysof HelloWorld>(key: K): K{
    return key;
}

// 'y' has type '"hello"'
let y = check("hello");
check("hullo"); // error! "hullo" not assignable to keysof HelloWorld

A parameter or return type being a keysof type causes it to be contextually types as a string literal if need be. A keysof type resolves to a union of the string literal types of the keys of the given type when it is actually resolved (which it is resolved to for relationship checking). It is StringLike, and as such it has the apparent type of a string. It is effectively sugar for writing out the union type of all the keys of an object by hand, except it is also able to be generically instantiated.

cc @DanielRosenwasser

@alitaheri
Copy link

Also probably solves #7722.

How would classes and namespaces work with this? ( if they should in the first place 😅 )

and how about index signatures?

interface A {
  foo: string;
  [property: string]: any;
}

what will be keysof A look like? string ?

@weswigham
Copy link
Member Author

@alitaheri: keysof ClassName gets you the instance members, keysof typeof ClassName gets you the static members, no special handling required. Any ole' type is supported in the argument position (with the caveat that anonymous objects must be within parenthesis to disambiguate them from function bodies).

if (!type.resolvedBaseUnion) {
// Skip any essymbol members and remember to unescape the identifier before making a type from it
const memberTypes = map(filter(getPropertiesOfType(type.baseType),
symbol => !startsWith(symbol.name, "__@")),
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you split this into intermediate variables?

@wclr
Copy link

wclr commented Sep 1, 2016

When this going to landed?

@DanielRosenwasser
Copy link
Member

DanielRosenwasser commented Sep 1, 2016

@whitecolor We need to discuss this at a design meeting. There are small details that can make all the difference (such as what to do with index signatures).

@wclr
Copy link

wclr commented Sep 1, 2016

@DanielRosenwasser it would be cool if another part for #1295 would be implemented soon

@aluanhaddad
Copy link
Contributor

aluanhaddad commented Oct 1, 2016

I think this is a big step forward for the type system. The question I have, and sorry if this has already been answered, is this: What is the enumeration algorithm used?

  1. Is it the same as Object.keys?
  2. Is it the same as an unfiltered for..in?
  3. If neither of the above, how exactly does it work.

On the one hand, it could be very surprising to not get the property keys of super class properties, especially as subclassing becomes more and more fashionable. On the other, it would be equally surprising if it behaved differently from Object.{keys | entries | values}.

Personally, I would prefer it to only return enumerable own properties, but I almost never create class hierarchies in TypeScript.

@DanielRosenwasser
Copy link
Member

@aluanhaddad Problem is that enumerable own properties aren't encoded in the type system and there's not really a straightforward way we've thought of to add it. @sandersn ran into similar issues with the object rest & spread operators.

@aluanhaddad
Copy link
Contributor

aluanhaddad commented Oct 1, 2016

@DanielRosenwasser thank you for clarifying.

It makes a lot of sense that the language does not track own properties because statically modelling the flexibility of JavaScript's composition mechanisms would seem to preclude doing so.

Perhaps if this operator walks the prototype chain a different name would be appropriate.

keysof is a really pleasant name, but if you read it aloud in a mathematical style it, sounds like calling Object.keys. I don't mean to be pedantic here, but it would be confusing if you use the latter frequently.

@remojansen
Copy link
Contributor

I created #11813 because I was not aware of this. I have closed it but it can still be used as a real world use case for the need of this operator.

@wycats
Copy link

wycats commented Oct 25, 2016

@aluanhaddad I can understand why you expect keysof and Object.keys to refer to the same concept (they both have the word "keys" in them, after all).

That said, I'd find it unfortunate if this very useful operator was restricted to enumerable-own properties.

Here's an example scenario I ran into today that got me thinking about this feature again: creating a hard-fail version of the index-for-friendly-access trick:

class X {
  constructor(private name: string) {}
}

let x = new X("Godfrey");
x['name'] // type checks, "Godfrey", type is string, as expected
x['nmae'] // type checks, undefined, type is any, unexpected

In other words, this trick conflates the elimination of privacy checks with a willingness to accept successful type-checking and any as a result if the property doesn't exist at all.

We could use the keysof operator to create a safer version of the same operator:

class X {
  constructor(private name: string) {}
}

let x = new X("Godfrey");
x['name'] // type checks, value is "Godfrey", type is string, as expected
x['nmae'] // type checks, value is undefined, type is any, unexpected

function get<T, K extends keysof T>(obj: T, key: K) {
  return obj[key];
}

let x = new X("Godfrey");
get(x, 'name') // type checks, "Godfrey", type is string (?), as expected
get(x, 'nmae') // fails to type check

Whether this works or not depends on whether keysof includes private or protected fields, and whether the type inferencer expands get correctly.

Regardless of those details, this illustrates that there may be valid use-cases for keysof working both against private/protected fields, against, non-enumerable fields, and against non-own fields.

My own perspective is that keysof should match the semantics of the no-implicit-any indexing operator: in other words, a key is in keysof (typeof val) if typeof val[key] is a type known to the type system. Pairing a keysof type check with the indexing operator seems eminently reasonable to me.

This might also argue for a few flavors of keysof, but matching semantics already encoded in the type system in some way seems like a pre-requisite for quick inclusion.

Finally, I also wonder whether not encoding enumerable/own in the type system will be practical in the long-term, especially considering the soon-to-be-included object-spread ({ ...a, ...b }) operator in JavaScript.

@aluanhaddad
Copy link
Contributor

@wycats I think you're points about overall usefulness outweigh my arguments about consistency with existing features. While I don't use classes particularly often I do find the object-spread example very compelling. In light of that feature, requiring ownness would kill a lot of the usefulness even in scenarios involving simple object literals. Also if manifest mixin support is ever added to ECMAScript, restricting it to enumerable own properties would also be very unfortunate.

I am convinced.

@ahejlsberg
Copy link
Member

Closing as this has been superseded by #11929.

@ahejlsberg ahejlsberg closed this Nov 2, 2016
@weswigham weswigham deleted the keysof-type-operator branch April 10, 2018 23:08
@microsoft microsoft locked and limited conversation to collaborators Jul 25, 2018
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

9 participants