Description
This strawman proposal aims to bring type-safety to a common "nested properties access" pattern in JavaScript libraries.
A common eww-case
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 (making their use perillous / brittle at best):
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);
To sleep better at night, I'd like to be able to declare something like:
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;
}
(see alternative syntaxes at the bottom)
Literal index types & type property access syntax
To achieve that, we 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
andconst 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]
istypeof t[i]
wheret: T
andi: I
- If
I
is a literal string type andT
has a property which name (or constant computed property key) isi
, thenT[I]
will have that property's type. - If
T
has an index operator that accepts keys of typeI
, thenT[I]
will be the return type of that operator. Properties are resolved before index operators ("property wins over index", TBC). - If
T
isany
,T[I]
isany
and a warning / error is emitted if--noImplicitAny
is set - Otherwise, an
No property ${i} found on type ${T}
error is emitted.
- If
-
For a literal index array type
A
of lengthn
(A <: (const string | const number)[]
):T[A]
yieldstypeof t[a[0]][a[1]]...[a[n - 1]]
wheret: T
anda: A
i.e.
T[[P0, P1... Pn]] = ((T[P0])[P1])...[Pn]
(applying rules for property access with a single literal index type above, one step at a time)
-
Where would it be useful?
To declare type signatures of lens-like APIs:
- Google's Closure Library: goog.object.getValueByKeys
- RxJS: Rx.Observable.pluck
- lodash: _.get
- Facebook's Immutable.js: Immutable.List.updateIn,
Immutable.Map.updateIn (may require some additional changes in Immutable.d.ts to get the signature useful & right, which is beyond the scope of this proposal) - nested-property: get
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 havingnumber
andstring
, 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)
Other ways to address that use-case?
I'm new to TypeScript and may have overlooked some nicer typeof
-way to do part or all of these things: please enlighten me and thanks for reading!