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

Suggestion: Allow string literals in index types #8336

Closed
christyharagan opened this issue Apr 27, 2016 · 3 comments
Closed

Suggestion: Allow string literals in index types #8336

christyharagan opened this issue Apr 27, 2016 · 3 comments
Labels
Duplicate An existing issue was already created

Comments

@christyharagan
Copy link

Problem

In the cycle.js project, data-flow is implemented by defining functions of the type:

type CreateDataFlow<Source, Sink> = (sources: Source)=>Sink

Where Source and Sink are interfaces where every member is a property of type Observable.

Each implementation of CreateDataFlow will specify Interfaces (typically without index types) for Source and Sink.

Currently, it is not possible to specify this contract using types.

This is a problem because the contract is not enforced by types, and instead relies on the developer to implement their data-flow function correctly. More specifically, this is a problem when dealing with higher-order code that takes implementations of CreateDataFlow, that wants to deal with the input/output generically (i.e. without knowing what the keys/members are at compile time, it wishes to iterate over them knowing only that the type is Observable).

Existing Solutions

There are two options:

The first is the one illustrated above. I.e., where the type-parameters Source and Sink have no type-restrictions. This creates the situation where any function that takes a parameter will be accepted. It's not clear from the typing what the contract should be, and so it will be trivially easy to pass in an invalidly typed function. If we wish to iterate over the output of the implementation, for example, we have to cast it to an index type (although, type guards do at least make this runtime safe).

The second option is to try something like:

type CreateDataFlow<Source extends {[key:string]:Observable<any>}, Sink extends {[key:string]:Observable<any>}> = (source:Source)=>Sink

The problem is, this doesn't actually adhere to the contract: If we try to use an implementation where Source is an Interface (with all members having type Observable), an error is thrown by the compiler. Using this approach we are forced to weaken the contract to index-types only, which means we cannot differentiate (at a type-level) between implementations, and indeed allows callers of an implementation of CreateDataFlow to pass in any conforming instance (although, again type-guards do make this runtime safe).

Neither solution adequately captures the contract, and requires use of type-guards to make type-safe. Needless to say, runtime type-safety is not nearly as nice as compile time, otherwise we'd all be using JavaScript instead.

Proposal: Allow string literals in index types

The proposal is that the following code would be valid:

type ObservableMap <Keys extends string> = {[keys:Keys]: Observable<any>}

And so the definition of CreateDataFlow becomes:

type CreateDataFlow<SourceKeys, SinkKeys> = (source: ObservableMap<SourceKeys>) => ObservableMap<SinkKeys>

Sub-types would be interfaces, and Key would be a string-literal defining the members, e.g.:

interface MySource {
  a: Observable<string>
  b: Observable<number>
}
type MySourceKey = 'a'|'b'

interface MySink {
  c: Observable<boolean>
  d: Observable<void>
}
type MySinkKey = 'c'|'d'

const myDataFlow: CreateDataFlow<MySourceKey, MySinkKey> = (mySource: MySource)=>MySink {
  // ...
}

Potentially something akin to #7722 could be used to manage these "key" definitions.

@malibuzios
Copy link

malibuzios commented Apr 27, 2016

There's a proposed type operator called memberof, that takes a [non-primitive] type and converts it to a corresponding string literal union of its members, I thought it would be interesting to try it here:

type CreateDataFlow<Source extends {[key: memberof Source]: Observable<any>}, Sink extends {[key: memberof Sink]: Observable<any>}> = 
  (source: Source) => Sink;

This requires a relatively unusual, recursive-looking, self reference in the constraints, e.g. Source extends {[key: memberof Source]: Observable<any>} but I wondered if it might be technically possible?

@malibuzios
Copy link

malibuzios commented Apr 28, 2016

Alright, I've made some tests:

This:

function test<T extends T>() {}

Gives a 'circular reference' error.

But this doesn't:

function test<T extends T[]>() {}
test<number>();

Instead it gives the error "type number does not satisfy the constraint number[]" so it seems like the self reference is evaluated based on the type argument that it is assigned, this leads me to believe that the approach with memberof would probably work.

And it is already possible to construct a self-referencing constraint that does not generate an error:

function test<T extends T | T[]>() {}
test<number>(); // No error

Now let's go back to the original intention. I'll try to rewrite it a bit differently:

type SimilarMembersButWithType<Type, Container> = { [key: memberof Container]: Type };

type CreateDataFlow<Source extends SimilarMembersButWithType<Observable<any>, Source>, Sink extends SimilarMembersButWithType<Observable<any>, Sink> = 
  (source: Source) => Sink;

extends SimilarMembersButWithType<Type, Container> seems like a form of a universal quantification:

∀(member ∈ Container), typeof member ≤ Type

It would be interesting to think what else can be done with this pattern, one thing is using a union in the expected type to express possibilities of types:

declare function func<T extends SimilarMembersButWithType<number | string, T>>(arg: T);

func({ a: 1, b: 2, c: "abc" }); // works;
func({ a: 1, b: 2, c: "abc", d: true }); // errors;

Edit 1: I changed the alias name to SimilarMembersButWithType which means something like "Create a index signature that only includes the keys of the given container, but with this particular type for the properties", so it more like a type "transformation" expression. It only becomes a predicate when used in conjunction with extends ...

Edit 2: corrected typeof member = Type to typeof member ≤ Type meaning 'typeof member is covariant to Type'.

@DanielRosenwasser
Copy link
Member

DanielRosenwasser commented Jun 7, 2016

Sorry that this took so long to take a look at @christyharagan! There is actually another issue about this (#5683), so I'm going to close this as a duplicate, though I'm glad you took the time to give more motivating examples. Thanks!

Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
Duplicate An existing issue was already created
Projects
None yet
Development

No branches or pull requests

3 participants