Description
This is a discussion thread where I'd like to give a high-level overview of the type-level operations (as opposed to expression-level) that we can and can not yet do today.
This differs from the TS roadmap by identifying holes, while complementing the issues list by trying to show some of the bigger picture, the goal being to see what issues tie into which points, and how we could address them.
I'd like stimulate discussion on how we could fill the holes here; for all I know there are holes we can find solutions to with no changes to TS!
Below is my list of imaginable basic type operations. The reason I focus on these is that, with basic operators down, most more complicated use-cases could be addressed simply by combining these. Names are based on my implementations here.
Additions / corrections / related issues / comments welcome!
Operations:
Built-in operators:
- union
|
: allow either of two types. also helps get the more lenient of two types, i.e.T | never
->T
. you'll encounter this in type inference since optional params yield| undefined
types. no known warts. - intersection
&
: get the stricter of two types, i.e.T & never
->never
. shouldn't need this very often. also helps combine two objects. warts:- overlapping keys when combining objects will
&
their contents too (alt:Overwrite
/MergeAll
)
- overlapping keys when combining objects will
keyof
: create a union of string literals from a type's keys. warts:- returning a string union means number literal keys get converted to string literals
- returning a string union means symbol keys get ignored
- when a string index is present, there is no known way to get the individual string keys
- somehow also gives prototype keys for array types, yet not for object types
in
: construct an object type based on a union of strings (keys) and corresponding calculated values based on these. warts:- since this is based on strings, anything else, such as numbers, symbols, or indices, needs to be specified separately (->
&
).
- since this is based on strings, anything else, such as numbers, symbols, or indices, needs to be specified separately (->
- member access: get the type at a certain index of an object/tuple type (considering intersections as a single object, getting multiple result for union'd objects). warts:
- there is ambiguity w.r.t. whether the prototype or the index should trump. it currently lets the prototype trump, even if you wanted it not to, meaning you're likely to get unexpected behavior for member access operations for e.g.
toString
. - does implicit conversion between numeric literals and string literals, not unlike JS. not a problem though. :)
- there is ambiguity w.r.t. whether the prototype or the index should trump. it currently lets the prototype trump, even if you wanted it not to, meaning you're likely to get unexpected behavior for member access operations for e.g.
&
is a bit less straight-forward from the rest in its use-cases:
- a way to make impossible types like
string & number
(uses?) - a redundant way to write
never
(T & never
), which gets more useful given conditionals, but then you could just use those to conditionally producenever
right away - a poor man's
Overwrite
/MergeAll
(inferior in its behavior intersecting types in overlapping keys, which poorly reflects actual JS). if you know keys won't overlap though, it's great since it's short, built-in and performant. - 'get the most specific type of these two': haven't found use-cases for this, but could also be done given
Matches
- add a symbol to an existing object
- have an index type with keys not bound to sub-classing it
Boolean operations:
-
Not
,And
,Or
,Eq
,Neq
Note: these can currently be implemented through string literal representations. It would be possible to convert these to boolean literals (StringToBool
), but cannot yet map boolean literals to these (BoolToString
) or other values for that matter.
Array (tuple) operations
Unary:
-
TupleLength
: check the length of a given tuple type. -
ArrayProp
: get the element type for a homogeneous array type (similar for extracting generics from other parameterized types)
Binary:
[x]TupleProp
: get the type at a certain index for a tuple/array type. justT[I]
.-
TupleHasIndex
: check whether a tuple type contains a given index. -
TupleHasElem
: check whether a tuple type contains a given type among its elements. This could be done givenTypesEq
. - concatenate / append / prepend. possible today using numeric objects (compatible with
ArrayLike
), but could become possible for tuple types natively with the variadic kinds proposal at Proposal: Variadic Kinds -- Give specific types to variadic functions #5453.There has been talk there this would also depend on Proposal: strict and open-length tuple types #6229. - destructuring tuples: see above
- difference: remove indices in one tuple type from another tuple type. see above.
-
Vector
: create a tuple type for a given element type plus size.
Advanced:
- array iteration, used for e.g.
TupleLength
-
reduce
: the function needsReturnType
for its dynamic reducer functions; otherwise doable using iteration. see Proposal: add a built-inreduce
function on the type level #12512. -
map
over tuples: doable now through numerical objects for fixed conditions; also needsReturnType
in case the mapping function is given as a function (e.g.map
itself).
object operations
Unary:
-
ObjectLength
: check the length (number of keys) of a given heterogeneous object type. doable givenUnionLength
or (object iteration +Inc
).
Binary:
-
ObjectProp
(need to test): get the type at a certain index for an object type. Normally one would just useT[K]
, which offers the desired behavior if one expects prototype methods liketoString
to prioritize the prototype over the string index. If one instead expects these to trigger the string index, you'd want this instead. -
ObjectHasStringIndex
: check whether an object has a generalstring
key, e.g.[k: string]: any
. -
ObjectHasNumberIndex
: accessing it works or throws, not sure how to check presence though. -
ObjectNumberKeys
: anumber
variant ofkeyof
. could be pulled off given union iteration (Partial
-> iterate to filter / cast back to number literals)... but still hard to scale past natural numbers. -
ObjectSymbolKeys
: aSymbol
variant ofkeyof
. no clue how to go about this unless by checking a whitelisted set such as those found in standard library prototype. this feels sorta useless though. -
ObjectHasKey
: check whether a heterogeneous object type (-> like{ a: any }
as opposed to{ [k: string]: any }
) contains a given key. -
ObjectHasElem
: check whether a heterogeneous object type contains a given type among its elements. This could be done givenTypesEq
. -
Overwrite
: merge objects, overwriting elements of the former by that of the latter. see #12215. -
Omit
(#12215): remove certain keys from a given object type. -
ObjectDifference
: remove all keys from an object that are part of a second object. -
IntersectionObjects
: filter an object to the keys also present in another object. -
FilterObject
: can be done already for fixed conditions; using a predicate function needsReturnType
Advanced:
-
map
over heterogeneous objects: probably just needsReturnType
. - object iteration: useful for e.g.
ObjectToArray
. This could enable union iteration, or the other way around.- One strategy that comes to mind relies on converting keys to tuple (given
UnionToArray
) then using array iteration. Alternatively, break string literals into characters, convert to numbers, convert objects to a nested version with one key at each stage using key sort, which could then be traversed in order... Nope, no member access on string literals.
- One strategy that comes to mind relies on converting keys to tuple (given
Type operations
Type checks (binary):
-
Matches
: check whether a given type matches another type (inclusive, e.g. true forstring
andstring
). This could be done givenReturnType
. -
TypesEq
: check whether two types are 'equal', that is, A satisfies B and vice versa. This could be done givenMatches
. -
InstanceOf
: check whether a given type represents a subset of another type (-> exclusive match). This could be done givenMatches
. -
PrototypeOf
: get the prototype (-> methods) of a type.Partial
helps, thoughSymbol
-based keys get killed.
Type casts (unary):
-
StringToBool
: can be implemented manually given the limited options, mapping desired keys totrue
/false
, potentially having anything else fall back toundefined
/boolean
. string literals are used in the boolean operators above, while boolean literals are useful in e.g. type guards (expression-level if/else). -
BoolToString
-- mapping from non-strings could be done givenReturnType
. -
StringToNumber
-- convert a numerical string literal to an actual number literal, doable using a whitelist (doesn't scale well to higher numbers). -
NumberToString
-- convert a number literal to a numerical string literal, doable using a whitelist (doesn't scale well to higher numbers). -
UnionToObject
-- use a union of string literals as object keys, possible with e.g.{ [P in Union]: P }
. -
UnionToArray
: could be done given e.g. union iteration. -
ObjectToArray
: could be useful if converting tuples types to number-indexed object types, do further operations, then convert back. likely needs object iteration. -
ObjectKeysToUnion
--keyof
does this. -
ObjectValsToUnion
: just plug the keys back into the object -
TupleToObject
: convert a tuple type to an object type (both number/string indices work), cleaning out prototype methods. -
TupleToUnion
: convert a tuple type to a union of types. -
TupleIndicesToUnion
: get the indices of a tuple type as a union of numerical strings.
Union operations
Unary:
- a way to access union elements, e.g. going from
"a" | "b" | "c"
to"a"
. this could enable union iteration usingDiff
if they're all string literals, which in turn could enable object iteration. or the other way around. -
IsUnionType
-- solvable today only for unions consisting of known sets of keys, see myIndeterminate
; a proper solution could be made using union iteration or a way to access arbitrary / random elements (e.g. with conversion to tuple type) -
UnionLength
: check the length of a union, i.e. how many options it is composed of.
Binary:
- union:
A | B
-
UnionHasKey
: check whether a union of string literals contains a given key. -
UnionHasType
: general case, check whether a union of arbitrary types contains a given type.- could be achieved using
TypesEq
. plugging a union into it should return e.g."0" | "1"
in case it contains a match -- at that pointUnionHasKey
works.
- could be achieved using
-
IntersectionUnions
: get the intersection of two union types, possible today given unions of string literals. -
DifferenceUnions
: subtract any keys from one union from those contained in another union. -
UnionContained
: verify whether one union is fully contained in another.
Advanced:
- union iteration: helps implement
UnionToArray
,IsUnionType
. could be achieved givenUnionToArray
or a way to access elements from a union. This could enable object iteration, or the other way around.
intersection operations
function/parameter operations
-
ReturnType
: get the return type of function expressions -- Proposal: Get the type of any expression with typeof #6606 (dupes: Suggestion: Using typeof with an expression #4233, Type query for a result of a function call #6239, Get return type of interface function #16372) - conversion of parameters from/to tuple types: see variadic kinds at Proposal: Variadic Kinds -- Give specific types to variadic functions #5453
- function composition -- still issues with generics, see Supporting generic type inference over the other higher-order functions #9366. current approach relies on overloads; might be alleviated as part of variadic kinds, see above.
- currying: see function composition.
- conditionally throwing 'custom' errors: given
ReturnType
, apply a function with arguments that would not match its requested param types - pattern matching: given
ReturnType
, use overloaded type-level function application to emulate pattern matching from other languages. - constraints: e.g. divisor of a division function may not be
0
. given pattern matching (above), just add an extra generic to said division function using a default with pattern matching to only resolve for non-0
input, e.g.function div<B extends number, NotZero = { (v: '1') => 'whatever'; }({ (v: 0) => '0'; (v: number) => '1'; }(B))>(a: number, b: B)
.
operations on primitives (string/number/boolean literals)
These are currently considered out of scope, see #15645.
That said we can do a bit with natural numbers:
- Numbers:
Inc
,Dec
,Add
,Subtract
,Mult
,Pow
,DivFloor
,Modulo
, comparators:Gt
(>
),Lt
(<
),Gte
(>=
),Lte
(<=
)
Strings:
- member access on string literals
- appending string literals
Progress:
Type | Member Access | Manipulation | Iteration |
---|---|---|---|
Tuple | ⭕ | ⭕ (as numerical objects until #5453) | ⭕ |
Object | ⭕ | ⭕ | ❌ |
Union (of string literals*) | ❌ | ⭕ | ❌ |
Function | ❌ (#6606) | ❌ (#5453) | n/a |
Bool | n/a | ⭕ (as strings) | n/a |
Number | n/a | ⭕ (low-ish natural numbers) | n/a |
String | ❌ | ❌ (#15645) | n/a |
*: union operators are pretty much limited to unions of string literals as it stands, as the only basic operators on unions (in
+ member access) both operate exclusively on these.
Not listed: type-level type checks (also need #6606)
Top features needed:
- type-level function application #17961 type level function application, needed for
BoolToString
,map
over tuples / heterogeneous objects,FilterObject
,reduce
, function composition - Allow calls to overloaded functions when all possible combinations of union type parameters resolve to valid overloads #17471 overload resolution / evaluation order issue: make
chooseOverload
consider type parameter values, not just their constraints. needed for:ReturnType
,Matches
/TypesEq
/InstanceOf
,ObjectHasElem
,TupleHasElem
, throwing errors, pattern matching, constraints,ObjectHasNumberIndex
. - tuple type spread #17884
[...a]
(tuple manipulation): can be emulated with numerical objects, but they lack methods. - extract rest params #17898
(...args: Args) =>
(capturing params) - WIP: type call spreads #18007
Fn(...Args)
- relevant for e.g. composition,curry
andbind
. - consider spread tuples when matching signature #18004 spread tuples
-
...
from union into tuple: casting union/object to tuple. note this one is tougher in that order is technically undefined. Challenges this would address includeUnionLength
,ObjectLength
,UnionHasType
,UnionToArray
,ObjectNumberKeys
, union member access, andObjectToArray
. this last one helps type e.g.R.toPairs
; the current compromise alternativeArray<a|b|c>
there is less useful since it can't be iterated over (for e.g.map
). - add compiler flag to enable granular inference outside var/let #17785 - skip automatic type widening for
const
/ params #16072 - generics erased