Proposal: Typed Hash

Topics: Language Specification
Oct 31, 2012 at 11:40 PM
Edited Nov 1, 2012 at 3:41 AM

JS libraries quite often use so-called "hashes", objects with all property values of the same type. For instance:

var myEventHandlers = {
    bind: function () { /*...*/ },
    change: function () { /*...*/ }
};

Here all properties are functions. In general properties names are arbitrary. Let's call such an object as Typed Hash.

I asked Anders Hejlsberg during the //build/ live session about plans to support it, and he replied that I can do it using indexer, for instance:

interface FunctionHash {
  [name: string]: Function;
}

However this interface doesn't express the semantics that I would like to achieve because it does not require property values to be Functions. Consider:

function bind(handlers: FunctionHash) {
  // ...
}

bind({ a: 1 }); // compiles. Shouldn't

Consider adding the following construct:

interface HashOfT {
  *: T;
}
  1. It inherits the [name: string]: T semantics
  2. Constraints all new properties to be of type T.
  3. Prevents "Member not found" compiler error.

By saying new property I mean that the constraint does not apply to the inherited properties, such as toString. The "asterisk type" is used when TS compiler usually throws a "Member not found" compilation error, but also it prevents defining new properties of different types.

With this construct I will not be able to pass a mistyped hash into a function:

function bind(handlers: { *: Function; }) {
}

bind({ a: 1 }); // compilation error: "a" is not a Function

Here { a: 1 } attempts to define a new numeric property a which is a violation of the constraint, so it should cause a compilation error.


When TS gets generics in the next version, type Hash<T> can be a shortcut for { *: T }.

Nov 1, 2012 at 3:34 AM

The proposed "asterisk type" implies that

  1. Properties/methods cannot be defined in a type if there is an "asterisk type". 
  2. Same applies to subtypes and interface extensions: asterisk types are not overriddable (in some sense for the reason why arrays are not inheritable)
  3. Same applies to "partial types". For instance, it is incorrect to define interface String { *: number } because String already has members
Coordinator
Nov 1, 2012 at 3:29 PM

To my understanding, this largely comes from the limitation of JavaScript/ECMAScript itself.  You can't write an indexer on an object after the fact, instead you start with either an object (where [key:string]) or an array (where [key:indexing number]) and then build from there. 

I agree that the return type seems a bit fishy, as '1' doesn't have the type Function.  This may be a bug in the current implementation (or something subtle in the spec).

Nov 14, 2012 at 2:05 AM

I don't think it is a bug in the current implementation because it is correct from the specification perspective. 

In the case of the bind function the goal is to restrict the argument value to contain only property values of a certain type. Indexers do not require that. 

I think that the pattern of a typed as hash is quite popular in JS libraries and TypeScript simply lacks a support of that.

Here is another example. Imagine a function that returns a hash of columns:

interface Column {
  type: string;
  displayName: string;
}

function getProductColumns(): { *: Column } {
  return ...
}

var unitPriceColumn = getProductColumns().unitPrice;

Right now I have to define getProductColumns return type as any and so all its properties are any too. I want the unitPriceColumn variable to be of type Column (I know that I can cast it manually, but I don't want it to be automatic).

-Nodir

Coordinator
Nov 14, 2012 at 3:44 PM

To make sure I understand, you're suggesting a syntactic sugar for property access.  Currently, you can do this:

interface Column {
  type: string;
  displayName: string;
}

function getProductColumns(): {[field:string]:Column;} {
  return {"unitPrice": {type:"type", displayName:"displayName"}};
}

var unitPriceColumn = getProductColumns()["unitPrice"];

We just don't handle using a dot-style access on the last line and instead use the array style.  You do get intellisense if you dot between the last ']' and the ';'

 

Nov 14, 2012 at 6:20 PM
Edited Nov 14, 2012 at 6:27 PM

It's not only about syntax, see below. Admit that it is unnatural to write obj["prop"] when you can write obj.prop. I would rather write <DesiredType>obj.prop to specify the type but I would expect TypeScript to do it for me. I realize that according to specs indexers should not do that.

Again, the sample in my previous post is a second priority scenario. The main problem is that I cannot require a method parameter to be a typed hash. Here is a function that expects a hash of columns and a function call that violates the implied type restriction:

 

function somethingThatExpectsColumns(columns: { [key: string]: Column; }) {
  // ...
}

somethingThatExpectsColumns({ notAColumn: "a string" })

 

The best tool for this scenario that TS provides is indexers, but as I wrote earlier the problem is that an indexer actually does not require an object to have all properties of the same type. In this example, it does not require (does not raise a compilation error) when I pass an argument of an obviously wrong type. 

So, in other words, despite TS tries to defend developers from making "typing" mistakes, this particular scenario of a typed hash is not covered by TypeScript. I find it a problem in the type-system design.

That's why I propose a new element of the type-system. Think of a typed hash as a typed array, because both are collections and sometimes hashes are used instead of arrays. When you check whether an array instance is a sub-type of certain target array type (e.g. Column[]), you make sure that a type of every element is a sub-type of the target element type. Same here, but you should check property values of an object, not array elements.

Sincerely,
-Nodir

Coordinator
Nov 15, 2012 at 7:31 PM

That it doesn't give an error for your example then it is likely a bug in the compiler.  I'll copy this over to the issue tracker to confirm.

Coordinator
Nov 15, 2012 at 7:32 PM
This discussion has been copied to a work item. Click here to go to the work item and continue the discussion.