Skip to content

fleshing out type operators (discussion) #16392

Closed
@KiaraGrouwstra

Description

@KiaraGrouwstra

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)
  • 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 (-> &).
  • 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. :)

& 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 produce never 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. just T[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 given TypesEq.
  • 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 needs ReturnType for its dynamic reducer functions; otherwise doable using iteration. see Proposal: add a built-in reduce function on the type level #12512.
  • map over tuples: doable now through numerical objects for fixed conditions; also needs ReturnType 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 given UnionLength or (object iteration + Inc).

Binary:

  • ObjectProp (need to test): get the type at a certain index for an object type. Normally one would just use T[K], which offers the desired behavior if one expects prototype methods like toString 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 general string key, e.g. [k: string]: any.
  • ObjectHasNumberIndex: accessing it works or throws, not sure how to check presence though.
  • ObjectNumberKeys: a number variant of keyof. 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: a Symbol variant of keyof. 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 given TypesEq.
  • 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 needs ReturnType

Advanced:

  • map over heterogeneous objects: probably just needs ReturnType.
  • 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.

Type operations

Type checks (binary):

  • Matches: check whether a given type matches another type (inclusive, e.g. true for string and string). This could be done given ReturnType.
  • TypesEq: check whether two types are 'equal', that is, A satisfies B and vice versa. This could be done given Matches.
  • InstanceOf: check whether a given type represents a subset of another type (-> exclusive match). This could be done given Matches.
  • PrototypeOf: get the prototype (-> methods) of a type. Partial helps, though Symbol-based keys get killed.

Type casts (unary):

  • StringToBool: can be implemented manually given the limited options, mapping desired keys to true / false, potentially having anything else fall back to undefined / 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 given ReturnType.
  • 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 using Diff 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 my Indeterminate; 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 point UnionHasKey works.
  • 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 given UnionToArray or a way to access elements from a union. This could enable object iteration, or the other way around.

intersection operations

function/parameter operations

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:

Metadata

Metadata

Assignees

No one assigned

    Labels

    DiscussionIssues which may not have code impact

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions