Skip to content

Instantly share code, notes, and snippets.

@ochafik
Last active September 3, 2016 04:17
Show Gist options
  • Select an option

  • Save ochafik/a40546b6677fb95cda0d14371c545fb2 to your computer and use it in GitHub Desktop.

Select an option

Save ochafik/a40546b6677fb95cda0d14371c545fb2 to your computer and use it in GitHub Desktop.
Nested property access notation for types

This strawman proposal aims to bring type-safety to a common "nested properties access" pattern in JavaScript libraries.

Consider the following snippets that use RxJS & Immutable.js: they select / update nested properties using sequences of literal property names, and are currently impossible to model in a type-safe way:

const obs = Observable.of({a: {b: {c: 1}}});
obs.pluck('a', 'b', 'c') // Observable<number>

var nested1 = Immutable.fromJS({a: {b: {c: 1}}});
nested1.getIn(['a', 'b', 'c']) // number
var nested2 = nested1.updateIn(['a', 'b', 'd'], value => value + 1);

Intuitively, I'd like to declare the following (see bottom for alternative syntaxes):

interface Observable<T> {
  pluck<Props extends const string[]>(...names: Props): T[Props];
}

// Pseudo-code, may need more work:
interface Nested<T> {
  updateIn<Props extends const string[]>(
      keyPath: Props, updater: (value: T[Props]) => T[Props]): Immutable.List;
}
function fromNestedJs<T>(t: T): Immutable.Map & Nested<T> {
  return Immutable.fromJs(t) as any;
}

To achieve that, we'd could introduce the following notations / concepts in TypeScript (A <: B below means any value of type A can be assigned to variables of type B):

  • Literal index types const string and const number. These types sit between their respective literal types and primitive types:

    • 'foo' <: const string <: string (any string literal type is a literal index type)
    • 1 <: const number <: number (any number literal type is a literal index type)
  • Literal index array types (matching A <: (const string | const number)[]) are the types of arrays literals containing any mix of string literals and number literals. They obey the same rules as the other literal types:

    • [1, 'a'] is an literal index array type
    • [1, string] is not a literal index array type
    • [const string] is not a literal index array type
  • A property access notation for types.

    • Given a literal index type I (I <: (const string | const number)):

      T[I] is typeof t[i] where t: T and i: I

      • If I is a literal string type and T has a property named i where i is the literal value of I, then T[I] will have that property's type.
      • If T has an index operator that accepts keys of type I, then T[I] will be the return type of that operator. Properties are resolved before index operators ("property wins over index").
    • For a literal index array type A of length n (A <: (const string | const number)[]):

      T[A] yields typeof t[a[0]][a[1]]...[a[n - 1]] where t: T and a: A

Where would it be useful?

To declare type signatures of lens-like APIs:

Where would it be useless?

To implement those APIs: type checks will have to go through any at some point.

This is tailored for declarations only.

More examples

// Mix number and string literals:
const values: {a: number}[] = [{a: 1}, {a: 2}];
select(values, 1, 'a') // number

// Abuse tuples and mix with arrays:
const tuple: [number, [string[], [boolean]]] = [1, ['2', [true]]];
select(tuple, 1, 1, 0) // boolean
select(tuple, 1, 0, 10000) // string

Possible Extensions

There is no concept of literal symbol yet, but symbol could clearly make it in this proposal in the future:

const X = Symbol.for('X');
const value: {[X]: number} = {[X]: 1};
select(value, X); // number

Syntax concerns

function pluck<Props extends const string[]>(...names: Props): T[Props];

Potential issues:

  • Property access notation for types could become ambiguous if TypeScript ever adopts C-style fixed-size array types (e.g. number[8] for array of size 8), although tuples already fulfill many use-cases of fixed-size arrays.
  • const + types brings lots of memories from C++ development (where const types define some sticky / recursive immutability). Only having number and string, which are immutable types, mitigates that risk (even if ES2100 introduces const type modifiers).

Alternatives considered:

  • T[Props...]: my favourite alternative (distinct-enough from spread-syntax, yet reuses existing ... token)
  • T[...Props]: nope (too close to spread syntax and would conflict with the awesome variadic types proposal)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment