Skip to content

Suggestion: Allow string literals in index types #8336

Closed
@christyharagan

Description

@christyharagan

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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    DuplicateAn existing issue was already created

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions