any-ts is a small set of TypeScript namespaces that overload built-in names (like any
) to make them extensible.
Note
Currently, any-ts
is BYOI: "bring your own implementation". With the exception of the ANY_TS_VERSION
constant, any-ts
only ships type declarations.
- TypeScript v5.0 or greater
- The
strict
compiler option enabled in yourtsconfig.json
:
The reason any-ts
exists is because TypeScript types are hard to get right.
The TypeScript team has invested a lot of time into making their syntax beginner friendly, and easy to use.
The problem, as anyone who's used TypeScript for some time knows, is that while the type system is incredibly powerful, it's also incredibly 🦄 magical 🌈.
I think most TS users would agree:
While TypeScript is, in some ways, "easy" to use, it's not simple.
Some of this complexity, we feel, is avoidable. Let's look at an example.
Let's say you're writing a small library of helper functions:
/**
* `identity` is a function that takes any arbitrary input, captures as much type
* information as possible, and returns it to you:
*/
const identity = <const type>(term: type): type => term
/** `flatten` is a variadic function that takes a list of arrays and... well, flattens them. */
const flatten = <const T extends Array<Array<any>>>(...arrays: T) => arrays.flat()
You write some tests, everything works as expected, so you ship it. Nice work!
But before long, someone creates an issue. Here's what they tried to do:
import { identity, flatten } from "helpful-helpers"
const arr1 = identity([0, 1, 2, 3])
const arr2 = identity([4, 5, 6, 7])
const arr3 = flatten(arr1, arr2)
// ^^^^ 🚫 TypeError
Can you spot the problem?
The problem here is that arr1
has the type readonly [1, 2, 3]
, and flatten
only accepts mutable arrays.
You do some research and find it that every ReadonlyArray
, despite having the longer, more "qualified" name, does not extend Array
.
In fact, it's actually the other way around: every Array
is a structural subtype of ReadonlyArray
.
Here's how any-ts
solves this problem:
import type { any } from "any-ts"
const identity = <const type>(term: type): type => term
const flatten = <const T extends any.array<any.array>>(...arrays: T) => arrays.flat()
const arr1 = identity([0, 1, 2, 3])
const arr2 = identity([4, 5, 6, 7])
flatten(arr1, arr2)
// no type error
If you're troubled by the any.array<any.array>
syntax, don't worry: it's not as cursed as it might sound.
The short answer is that types and namespaces can share identifiers. This has been the case for years. And unlike objects, namespace can export types.
All we've done is overload any
, which allows us to piggyback on its semantics. any
is still any
, and any.array
is a type that absorbs the complexity (and verbosity) of readonly unknown[]
.
import type { any } from "any-ts"
type myStringArray = any.array<string>
// readonly string[]
With any-ts
, your intent is explicit:
import type { any, mut } from "any-ts"
type myMutableArray = mut.array
// ^? type myMutableArray = unknown[]
type myMutableStringArray = mut.array<string>
// ^? type myMutableStringArray = string[]
// or, if you prefer not importing both:
type myOtherArray = any.mut.array
// ^? type myOtherArray = unknown[]
type myOtherStringArray = any.mut.array<string>
// ^? type myOtherStringArray = string[]
In our opinion, this is where any-ts
really shines.
If you're a power user, you can do some pretty cool things with any-ts
. Let's continue using any.array
, even though it's one of the simplest types the library implements.
What if you wanted to extract (or pattern-match) the type that an array holds?
declare namespace Dictionary {
type fromArray<xs extends any.array>
= xs extends any.array<infer x> ? Record<string, x> : never;
}
type featureFlags = Dictionary.fromArray<boolean[]>
// ^? type featureFlags = Record<string, boolean>
Of the hundreds of types that any-ts
ships, almost all of them can be used in matching position, like above.
If you're looking for more examples, you can find more in the examples directory (WIP).
Looking for something that we don't ship yet? Raise an issue -- we'd love to try to help!
any
array
boolean
char
empty
Kind
, a simple but powerful HKT encoding that supports partial application & re-bindingIdentity
integer
mut
never
, a.k.a. "semantic never"nonempty
number
bigint
object
Path
some
string
traversable
traversal
Tree
- in particular, we believe our implementation of
Tree.fromPaths
to be the simplest solution + the solution with the least number of allocations possible
- in particular, we believe our implementation of
Union
Universal