Extract or modify pieces of arbitrarily nested types with type lenses.
npm install type-lenses
We are interested in needle in the type bellow
type Haystack = Map<string, {foo: [(f: (arg: string) => needle) => void, 'bar'] }>;We write a path pointing to our needle:
import { Lens, free, a, r } from 'type-lenses';
type FocusNeedle = Lens<[free.Map, 1, 'foo', 0, a, r]>;In plain English, the steps are as follows:
- Focus on the type
Map - Focus on the argument at index
1in the arguments list - Focus on the field
"foo"in the object - Focus on the element at index
0in the tuple - Focus on the first parameter of the function
- Focus on the return type of the function
We can extract our needle with Get:
import { Get } from 'type-lenses';
type Needle = Get<FocusNeedle, Haystack>;It results in:
needle
We can replace needle by any compatible type with Replace:
import { Replace } from 'type-lenses';
type YihaStack = Replace<FocusNeedle, Haystack, 'Yiha!'>;// result:
Map<string, {foo: [(f: (arg: string) => 'Yiha!') => void, 'bar'] }>Similarily to Replace, Over lets us replace needle with the result of applying it to a compatible ready-made or custom free type:
import { Over } from 'type-lenses';
type PromiseStack = Over<FocusNeedle, Haystack, free.Promise>;// result:
Map<string, {foo: [(f: (arg: string) => Promise<needle>) => void, 'bar'] }>FindReplace removes the need to construct a path, as long as we know what needle to target:
import { FindReplace } from 'type-lenses';
type YihaStack = FindReplace<Haystack, needle, ['Yiha!']>;// result:
Map<string, {foo: [(f: (arg: string) => 'Yiha!') => void, 'bar'] }>It also accepts a replace callback of type Type<[Needle, Path?]> if you need to run arbitrary logic:
// any unary free type can work
type Foo = FindReplace<{ a: 1, b: 2 }, number, free.Promise>;
import { $ReplaceCallback } from 'type-lenses';
import { Optional, Last, A, B } from 'free-types';
// or one of your design (don't freak out, see the doc)
interface $Callback extends $ReplaceCallback<number> {
type: this['prop'] extends 'a' ? Add<10, A<this>>
: this['prop'] extends 'b' ? Promise<A<this>>
: never
prop: Last<Optional<B, this>>
}
type Bar = FindReplace<{ a: 1, b: 2 }, number, $Callback>;// result:
type Foo = { a: Promise<1>, b: Promise<2> }
type Bar = { a: 11, b: Promise<2> }FindReplace gives control over the search, the number of matches and the way they are replaced, with some limitations to keep in mind. Make sure to read the documentation.
We can find paths with FindPath and FindPaths.
The former is guaranteed to return a single path pointing to needle, or never:
import { FindPath } from 'type-lenses';
type PathToNeedle = FindPath<Haystack, needle>;// result:
[free.Map, 1, "foo", 0, Param<0>, Output]Param<0>andOutputare aliases foraandr;free.Mapis a built-in free type, but you can also register your own so they can be inferred (see doc/$Type);- The behaviour for singling out a match is documented in doc/FindPath(s).
The latter returns a tuple of every path leading to needle. If it is self (the default), it returns every possible path, which can be useful for exploring a type.
import { FindPaths } from 'type-lenses';
type EveryPath = FindPaths<Haystack>;// result:
[[free.Map],
[free.Map, 0],
[free.Map, 1],
[free.Map, 1, "foo"],
[free.Map, 1, "foo", 1],
[free.Map, 1, "foo", 0],
[free.Map, 1, "foo", 0, r],
[free.Map, 1, "foo", 0, a],
[free.Map, 1, "foo", 0, a, a],
[free.Map, 1, "foo", 0, a, r]]FindPath(s) give control over the search and the number of matches, with some limitations to keep in mind. Make sure to read the documentation.
Finally, we can type check a query, for example in a function:
declare const foo: <
const Path extends readonly string[] & Check,
Obj extends object,
Check = Audit<Path, Obj>
>(path: Path, obj: Obj) => void;
foo(['q', 'b'], { a: { b: 42, c: 2001 }})
// ~~~ Type "q" is not assignable to type "a"This behaviour also enables reliable auto-completion:
foo([''], {a: { b: 42, c: 2001 }})
// -- suggest "a"
foo(['a', ''], {a: { b: 42, c: 2001 }})
// -- suggest "b" | "c"Type Checking | Lens | Query | Type | Get | GetMulti | Replace | Over | FindReplace | FindPath(s) | Audit | Free utils
The library type checks your inputs in various ways, but these checks never involve the Haystack.
This is because the type checker fails to check generics, even with adequate type constraints. Since working with a generic Haystack is a very common use case for lenses, I chose to ignore this check for ease of use.
If you need to check your query, you can do so with a Lens.
Lens<Query, Model?>
You can create a lens by passing a Query to Lens.
type A = Lens<1>;
type B = Lens<['a', 2, r]>;Utils such as Get or Replace promote every Query to a Lens, but it is advised to work with lenses when you want to reuse or compose paths.
Composing lenses is as simple as wrapping them in a new Lens:
type C = Lens<[A, B]> // Lens<[1, 'a', 2, r]>Lens optionally takes a Model against which to perform type checking.
type Haystack = Map<string, {foo: [(f: (arg: string) => needle) => void, 'bar'] }>;
type FocusNeedle = Lens<[free.Map, 1, 'bar', 0, a, r], Haystack>;
// ~~~~~~~~~~~~~~~~~~~~~~~~~~~
// Type '[$Map, 1, "bar", 0, a, Output]' does not satisfy the constraint '[$Map, 1, "foo", ...QueryItem[]]'.
// Type at position 2 in source is not compatible with type at position 2 in target.
// Type '"bar"' is not assignable to type '"foo"'A Query is a Lens, a Path or a PathItem
A Path is a tuple of PathItem.
Path items can be one of the following types:
| type | description |
|---|---|
Lens |
As we have already seen, nested lenses are flattened. |
Param<index>or a, b, ...f |
Focuses on a parameter of a function. |
Output or r |
Focuses on the return type of a function.
|
string |
Focuses on the field of a given name in an object. |
number |
Focuses on the item at a given index in a tuple. |
self |
By default it refers to the haystack, but some utils enable providing a value for it. |
$Type |
Focuses on the arguments list of a type for which $Type is a free type constructor. |
In a nutshell:
// A class we want to reference in a path
class Foo<T extends number> {
constructor(private value: T) {}
}npm install free-typesimport { Type, A } from 'free-types'
// A free type constructor for that class
interface $Foo extends Type<[A: number]> {
type: Foo<A<this>>
}
// Imagining a haystack where the needle is the first argument of Foo
type Haystack = Foo<needle>
// Our path would look like this
type FocusNeedle = Lens<[$Foo, 0]>;
type Needle = Get<FocusNeedle, Haystack>; // needle// We can also define a free utility type
interface $Exclaim extends Type<[string]> { type `${A<this>}!` }
type Exclamation = Over<['a'], { a: 'Hello' }, $Exclaim> // { a: "Hello!" }type-lenses re-exports a dozen built-in free type constructors under the namespace free.
If you want FindPaths to be able to find your own free types, you must register them:
declare module 'free-types' {
interface TypesMap { Foo: $Foo }
}See free-types for more information.
Return the queried piece of type or never if it is not found.
Get<Query, Haystack, Self>
| parameter | description |
|---|---|
| Query | a Query |
| Haystack | The type you want to extract a piece of type from |
| Self | A type which you want self to point to. It is Haystack by default |
The same as Get, but takes a tuple of Query and returns a tuple of results.
GetMulti<Query[], Haystack, Self>
Replace the queried piece of type with a new value in the parent type, or return the parent type unchanged if the query is never or doesn't match anything.
Replace<Query, Haystack, Value, Constraint?>
| parameter | description |
|---|---|
| Query | a Query |
| Haystack | The type you want to modify |
| Value | Any type you want to replace the needle with |
| Constraint | A mean of turning off type checking in higher order scenarios |
Value is type checked against the Query:
type Failing = Replace<[free.WeakSet, 0], WeakSet<{ foo: number }>, number>
// --------------- ~~~~~~
type Failing = Replace<[free.WeakSet], WeakSet<{ foo: number }>, [number]>
// ------------ ~~~~~~~~
// Type 'number' does not satisfy the constraint 'object'If Query is generic, you will have to opt-out from type checking by setting Constraint to any:
type Generic<Q> = Replace<Q, WeakSet<{ foo: number }>, object, any>
// ---Map over the parent type, replacing the queried piece of type with the result of applying it to the provided free type. Return the parent type unchanged if the query failed.
Over<Query, Haystack, $Type, Constraint?>
| parameter | description |
|---|---|
| Query | a Query |
| Haystack | The type you want to modify |
| $Type | A free type constructor |
| Constraint | A mean of turning off type checking in higher order scenarios |
The return type of $Type is fully type checked against the Query, however its parameters are only checked loosely for relatedness, because they also depend on the Haystack which is purposely excluded from type checking:
type Failing = Over<[free.WeakSet, 0], WeakSet<{foo: number}>, $Next>
// ~~~~~
// Type '$Next' does not satisfy the constraint 'Type<[Unrelated<number, object>]>'
type NotFailing = Over<[free.Set, 0], Set<'hello'>, $Next>
// will blow up with no error ------- -----If Query is generic, you will have to opt-out from type checking by setting Constraint to any:
type Generic<Q> = Over<Q, Set<1>, $Next, any>
// ---Find a Needle in the parent type and replace matches with new values, or return the parent type unchanged if there is no match.
The search behaves like FindPaths.
If there are more matches than you specified replace values, the last replace value is used to replace the supernumerary matches:
type WithValues = FindReplace<[1, 2, 3], number, [42, 2001]>;
// type WithValues = [42, 2001, 2001]Warning Do not expect object properties to be found and replaced in a specific order. If you need to find/replace multiple values in the same object, use a replace callback instead of a tuple of values.
FindReplace<Haystack, Needle, Values | $Type, From?, Limit?>
| parameter | description |
|---|---|
| Haystack | The type you want to modify |
| Needle | The piece of type you want to search |
| Values | $Type | A tuple of values to replace the matches with, or a replace callback |
| From | A path from which to start the search. |
| Limit | The maximum number of matches to find and replace |
If you use a replace callback, its first parameter must extend your Needle.
If you want to define a custom replace callback, you can extend $ReplaceCallback<T> which is really Type<[T, Path?]> where T is your Needle:
import { $ReplaceCallback } from 'type-lenses';
import { Optional, Last, A, B } from 'free-types';
interface $Callback extends $ReplaceCallback<number> {
type: this['prop'] extends 'a' ? Add<10, A<this>>
: this['prop'] extends 'b' ? Promise<A<this>>
: never
prop: Last<Optional<B, this>>
}
type WithCallback = FindReplace<{ a: 1, b: 2 }, number, $Callback>;
// type WithCallback = { a: 11, b: Promise<2> }The types Optional, A and B let you safely index this to extract the arguments passed to $Callback, while defusing type constraints.
Addexpects anumber, which is satisfied byA<this>;Lastexpects a tuple, which is satisfied byOptional<B, this>.
More information about these helpers in free-types' guide.
Here I also created a prop field for clarity, using Last to select the last PathItem in the Path.
FindPath is literally defined like so:
type FindPath<T, Needle, From extends Path = []> =
Extract<FindPaths<T, Needle, From, 1>[0], [any, ...any[]]>;FindPaths returns a tuple of every path leading to the Needle, or every possible path when Needle is self (the default).
The search results are ordered according to the following rules, ranked by precedence:
- Matches closer to the root are listed first (breadth-first search);
- Matches honour the ordering of tuples and function arguments lists;
- Matches do not honour the ordering of object properties;
- In function signatures, matched parameters are listed before any matched return type;
- the needles
any,neverandunknownmatchany,neverandunknownrespectively (useselfto match every path); - When the needle is
self, the ordering of paths which do not lead to aBaseType(a leaf) is unspecified.
type BaseType = string | number | boolean | symbol | undefined | void;FindPaths<T, Needle?, From?, Limit?>
| parameter | description |
|---|---|
| T | The type you want to probe |
| Needle | The piece of type you want selected paths to point to. It defaults to self, which selects every possible path. |
| From | A path from which to start the search. |
| Limit | The maximum number of matches to return |
From enables you to specify which path should be searched for potential matches. It can be used for disambiguation or to improve performance:
type PathsSubset = FindPaths<{ a: [1], b: [2] }, number, ['b']>
// type PathsSubset = [['b', 0]]Limit enables you to ignore matches which are of no interest to you. It also improves performance:
// provide an empty `From` to access this parameter vv
type PathsSubset = FindPaths<{ a: [1], b: [2] }, number, [], 1>
// type PathsSubset = [['a', 0]]Audit is the type being used internally to type check Lens, but it can be used with functions as well.
It returns a Suggestion which is a type related to a Query that can contain unions and be open-ended:
type Suggestion = Audit<['q', 'b'], { a: { b: number, c: number }}>;
// type Suggestion: ['a', ...QueryItem[]]
type Suggestion = Audit<['a', 'd'], { a: { b: number, c: number }}>;
// type Suggestion: ['a', 'b' | 'c']Success is represented either by QueryItem, QueryItem[] or readonly QueryItem[] depending on your input. You should not need to check for success, but if you do, consider using the companion type Successful which returns a boolean:
type OK = Successful<Audit<['a', 'b'], { a: { b: number, c: number }}>>;
// type OK: trueAudit<Query, Model>
| parameter | description |
|---|---|
| Query | The Query you want to type check |
| Model | The type that should to be traversable by the Query |
Be mindful that type-checking the query will make your function unusable in higher order scenarios.
declare const foo: <
const Path extends readonly string[] & Check,
Obj extends object,
Check = Audit<Path, Obj>
>(path: Path, obj: Obj) => void;
const bar = <
const Path extends readonly string[],
Obj extends object
>(path: Path, obj: Obj) => foo(path, obj)
// cryptic error ~~~~An obvious workaround is to check the input in bar and pass the check to foo as a type parameter:
const bar = <
const Path extends readonly string[] & Check,
Obj extends object,
Check = Audit<Path, Obj>
>(path: Path, obj: Obj) =>
foo<Path, Obj, Check>(path, obj)Alternatively, you could make type checking optional:
/** pass `any` to `_` in order to disable type-checking*/
declare const foo: <
const Path extends readonly string[] & Check,
Obj extends object,
Check = Audit<Path, Obj>
>(path: Path, obj: Obj, _?: Check) => void;
// ---------
const bar = (path: readonly string[], obj: object) =>
foo(path, { a: { b: null }}, null as any)
// -----------Free versions of Get, GetMulti, Replace and Over.
Can be used like so:
import { apply } from 'free-types';
type $NeedleSelector = $Get<Query>
type Needle = apply<$NeedleSelector, [Haystack]>;