Skip to content

Commit

Permalink
feat: separates empty from nonempty
Browse files Browse the repository at this point in the history
docs: documents `empty` and `nonempty` namespaces
feat: adds simplified `TypeError` module (`type-error/type-error.ts`)
todo: creates this todo: #132
  • Loading branch information
ahrjarrett committed May 21, 2024
1 parent d3db1fc commit cdf6897
Show file tree
Hide file tree
Showing 24 changed files with 342 additions and 46 deletions.
2 changes: 1 addition & 1 deletion src/array/array.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import type { any } from "../any/exports.js"
import type { check, typecheck } from "../check/exports.js"
import type { TypeError } from "../exports.js"
import type { nonempty } from "../empty.js"
import type { nonempty } from "../nonempty/nonempty.js"
import type { queue } from "./queue.js"
import type { tuple } from "./tuple.js"

Expand Down
4 changes: 2 additions & 2 deletions src/array/tuple.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import type { _ } from "../util.js";
import type { any } from "../any/exports.js"
import type { nonempty } from "../empty.js"
import { empty } from "../empty.js";
import type { nonempty } from "../nonempty/nonempty.js"
import { empty } from "../empty/empty.js";

export declare namespace tuple {
export {
Expand Down
3 changes: 2 additions & 1 deletion src/boolean/boolean.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,8 @@ export {
boolean,
}

import { empty, nonempty } from "../empty.js"
import type { empty } from "../empty/empty.js"
import type { nonempty } from "../nonempty/nonempty.js"
import type { any } from "../any/exports.js"
import type { _ } from "../util.js"

Expand Down
284 changes: 284 additions & 0 deletions src/empty/empty.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,284 @@
import type { _ } from "../util.js"
import type { TypeError } from "../type-error/exports.js"

/**
* # {@link empty `empty 🕳`}
* `=================`
*
* {@link empty `empty`} is a namespace for empty types, and type constructors that
* target emptiness.
*
* The idea of `empty` is an important one in our field. It's easy to
* underestimate its importance and focus on the "happy" (nonempty) path.
*
* But we should talk about (and think through) both cases, and treat emptiness as
* a first-class concern, rather than as an after-thought.
*
* But I'm probably preaching to the choir here, so enough talk, let's just dig in.
*/
export declare namespace empty {
export type path<type = readonly [], debug = never>
= [type] extends [[]] ? []
: [type] extends [readonly []] ? readonly []
: [debug] extends [never] ? never
: TypeError.new<"Expected an empty path", type>

export {
/**
* ## {@link empty.string `empty.string`}
*
* - Evaluates to the empty string type (`""`) when used as a nullary type
* - Functions as a pattern matcher when used as a unary type constructor
* - Functions as a constraint when applied to a type parameter
* - See also: {@link empty.string.debug `empty.string.debug`}
*
* @example
* ////////////////////
* // HAPPY PATH
* type ex_01 = empty.string
* // ^? type ex_01 = ""
* type ex_02 = empty.string<"">
* // ^? type ex_02 = ""
* type ex_03 = empty.string.debug<"">
* // ^? type ex_03 = ""
* const ex_04 = empty.string("")
* // ^? const ex_04: ""
* ////////////////////
*
* ////////////////////
* // ERROR PATH
* type ex_05 = empty.string<"101 Dalmations">
* // ^? type ex_05 = never
* //
* // 🚫 Argument of type 'string' is not assignable to parameter of type 'never'
* // ↓↓↓
* const ex_06 = empty.string("Cruella de Vil")
* // ^? const ex_06: never
* ////////////////////
*
* /////////////////////
* // ADVANCED USE
* //
* // You can use `empty.string` as a type-level optimization.
* /
* // For example, you could create an overload that short-circuits an expensive
* // type-level operation by handling the edge case separately:
* function stringToChars<T extends empty.string<T>>(emptyString: T): []
* function stringToChars<T extends any.string>(string: T): ExpensiveTypelevelComputation<T> // ...
* /////////////////////
*/
string_ as string
}

export type string_<type = "", debug = never>
= [type] extends [""] ? ""
: [debug] extends [never] ? never
: TypeError.new<"Expected an empty string", type>

export function string_<const T extends empty.string<T>>(empty: T): T
export namespace string_ {
/**
* ## {@link empty.string.debug `empty.string.debug`}
*
* - Evaluates to the empty string type (`""`) when used as a nullary type
* - Functions as a pattern matcher when used as a unary type constructor
* - Functions as a constraint when applied to a type parameter
* - Unlike {@link empty.string `empty.string`},
* {@link empty.string.debug `empty.string.debug`} _raises_ a `TypeError`
* when it encounters anything but (`""`)
* - Unlike {@link empty.string `empty.string`},
* {@link empty.string.debug `empty.string.debug`} **evaluates to** a
* `TypeError` when it encounters anything but (`""`)
* - See also: {@link empty.string `empty.string`}
*
* @example
* ////////////////////
* // ERROR PATH
* // 🚫 TypeError<[msg: "Expected an empty string", got: "101 Dalmations"]>'
* // ↓↓↓
* type ex_01 = empty.string.debug<"101 Dalmations">
* //
* // ^? type ex_01 = TypeError<[msg: "Expected an empty string", got: "101 Dalmations"]>
* // 🚫 TypeError<[msg: "Expected an empty string", got: "Cruella de Vil"]>
* // ↓↓↓
* const ex_02 = empty.string.debug("Cruella de Vil")
* // ^? const ex_02: TypeError<[msg: "Expected an empty string", got: "Cruella de Vil"]>
* ////////////////////
*/
type debug<T extends empty.string<T, "debug">> = empty.string<T, "debug">
function debug<const T extends empty.string<T, "debug">>(empty: T): T
}

export {
/**
* ## {@link empty.object `empty.object`}
*
* - Evaluates to the empty object type (`{}`) when used as a nullary type
* - Functions as a pattern matcher when used as a unary type constructor
* - Functions as a constraint when applied to a type parameter
* - See also: {@link empty.object.debug `empty.object.debug`}
*
* @example
*
* ////////////////////
* // HAPPY PATH
* type ex_01 = empty.object
* // ^? type ex_01 = {}
* type ex_02 = empty.object<{}>
* // ^? type ex_02 = {}
* ////////////////////
*
* ////////////////////
* // ERROR PATH
* type ex_03 = empty.object<{ dalmatians: 101 }>
* // ^? type ex_03 = never
* ////////////////////
*
* /////////////////////
* // ADVANCED USE
* //
* // You can use `empty.object` as a type-level optimization.
* //
* // For example, you could create an overload that short-circuits an expensive
* // type-level operation by handling the edge case separately:
* function mergeDeep<T extends empty.object<T>>(emptyObject: T): empty.object
* function mergeDeep<T extends any.object>(object: T): ExpensiveTypelevelComputation<T> // ...
* /////////////////////
*/
object_ as object
}

export type object_<type = {}, debug = never>
= [keyof type] extends [never] ? {}
: [debug] extends [never] ? never
: TypeError.new<"Expected an empty object", type>
export function object_<const T extends empty.object<T>>(empty: T): T
export namespace object_ {
/**
* ## {@link empty.object.debug `empty.object.debug`}
*
* - Evaluates to the empty object type (`{}`) when used as a nullary type
* - Functions as a pattern matcher when used as a unary type constructor
* - Functions as a constraint when applied to a type parameter
* - Unlike {@link empty.object `empty.object`},
* {@link empty.object.debug `empty.object.debug`} **raises** a `TypeError`
* when it encounters anything besides (`{}`)
* - Unlike {@link empty.object `empty.object`},
* {@link empty.object.debug `empty.object.debug`} **evaluates to** a
* `TypeError` when it encounters anything besides (`{}`)
* - See also: {@link empty.object `empty.object`}
*
* @example
* ////////////////////
* // HAPPY PATH
* type ex_01 = empty.object.debug<{}>
* // ^? type ex_01 = {}
* //
* const ex_02 = empty.object({})
* // ^? const ex_01: {}
* ////////////////////
*
* ////////////////////
* // ERROR PATH
* //
* // 🚫 TypeError<[msg: "Expected an empty object", got: { dalmatians: 101 }]>
* // ↓↓↓
* type ex_03 = empty.object.debug<{ dalmatians: 101 }>
* // ^? type ex_03 = TypeError<[msg: "Expected an empty object", got: { dalmatians: 101 }]>
* //
* // 🚫 TypeError<[msg: "Expected an empty object", got: { villain: "Cruella de Vil" }]>
* // ↓↓↓
* const ex_04 = empty.object.debug({ villain: "Cruella de Vil" })
* // ^? const ex_04: TypeError<[msg: "Expected an empty object", got: { villain: "Cruella de Vil" }]>
* ////////////////////
*/
type debug<T extends empty.object<T, "debug">> = empty.object<T, "debug">
function debug<const T extends empty.object<T, "debug">>(empty: T): T
}

/**
* ## {@link empty.array `empty.array`}
*
* - Evaluates to the empty array type (`[]`) when used as a nullary type
* - Functions as a pattern matcher when used as a unary type constructor
* - Functions as a constraint when applied to a type parameter
* - See also: {@link empty.array.debug `empty.array.debug`}
*
* @example
* ////////////////////
* // HAPPY PATH
* type ex_01 = empty.array
* // ^? type ex_01 = readonly []
* type ex_02 = empty.array<[]>
* // ^? type ex_02 = []
* ////////////////////
*
* ////////////////////
* // ERROR PATH
* type ex_03 = empty.array<["Perdita", "Pongo"]>
* // ^? type ex_03 = never
* ////////////////////
*
* /////////////////////
* // ADVANCED USE
* //
* // You can use `empty.array` as a type-level optimization.
* //
* // For example, you could create an overload that short-circuits an expensive
* // type-level operation by handling the edge case separately:
* function flattenDeep<T extends empty.array<T>>(emptyArray: T): empty.array
* function flattenDeep<T extends any.array>(array: T): ExpensiveTypelevelComputation<T> // ...
* /////////////////////
*/
export type array<type = readonly [], debug = never>
= [type] extends [[]] ? []
: [type] extends [readonly []] ? readonly []
: [debug] extends [never] ? never
: TypeError.new<"Expected an empty array", type>
export function array<const T extends empty.array<T>>(empty: T): T
export namespace array {
/**
* ## {@link empty.array.debug `empty.array.debug`}
*
* - Evaluates to the empty array type (`readonly []`) when used as a nullary type
* - Functions as a pattern matcher when used as a unary type constructor
* - Functions as a constraint when applied to a type parameter
*
* - Unlike {@link empty.array `empty.array`},
* {@link empty.array.debug `empty.array.debug`} **raises** a `TypeError`
* when it encounters anything besides (`[]` or `readonly []`)
* - Unlike {@link empty.array `empty.array`},
* {@link empty.array.debug `empty.array.debug`} **evaluates to** a
* `TypeError` when it encounters anything besides (`[]` or `readonly []`)
* - See also: {@link empty.array `empty.array`}
*
* @example
* ////////////////////
* // HAPPY PATH
* type ex_01 = empty.array.debug
* // ^? type ex_01 = readonly []
* type ex_02 = empty.array.debug<[]>
* // ^? type ex_02 = []
* const ex_03 = empty.array.debug([])
* // ^? const ex_03: []
* ////////////////////
*
* ////////////////////
* // ERROR PATH
* //
* // 🚫 TypeError_<[msg: "Expected an empty array", got: ["Perdita", "Pongo"]]>
* // ↓↓↓
* type ex_04 = empty.array.debug<["Perdita", "Pongo"]>
* // ^? type ex_04 = TypeError_<[msg: "Expected an empty array", got: ["Perdita", "Pongo"]]>
* //
* // 🚫 TypeError_<[msg: "Expected an empty array", got: ["Perdita", "Pongo"]]>
* // ↓↓↓
* const ex_05 = empty.array.debug(["Perdita", "Pongo"])
* // ^? TypeError_<[msg: "Expected an empty array", got: ["Perdita", "Pongo"]]>
* // ^? const ex_05: TypeError_<[msg: "Expected an empty array", got: ["Perdita", "Pongo"]]>
* ////////////////////
*/
type debug<T extends empty.array<T, "debug">> = empty.array<T, "debug">
function debug<const T extends empty.array<T, "debug">>(empty: T): T
}
}
1 change: 1 addition & 0 deletions src/empty/exports.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export type { empty } from "./empty.js"
2 changes: 1 addition & 1 deletion src/err/catch.spec.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
/** term-level import */ import { describe, expect } from "../test/exports.js"

import type { any } from "../any/exports.js"
import type { nonempty } from "../empty.js"
import type { nonempty } from "../nonempty/nonempty.js"

import type { Catch } from "./catch.js"
import type { mut } from "../mutable/exports.js"
Expand Down
2 changes: 1 addition & 1 deletion src/err/enforce.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ import type { Err, Err2 } from "./err.js"
import type { never } from "../never/exports.js"
import type { HasDiscriminant } from "../tag/tag.js"
import type { Union as U } from "../union/exports.js"
import type { empty } from "../empty.js"
import type { empty } from "../empty/empty.js"

declare namespace partial {
type strict<type extends globalThis.Partial<invariant>, invariant> = (
Expand Down
7 changes: 5 additions & 2 deletions src/exports.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
/** exports that come with an (empty) term-level equivalent */
export { empty, nonempty } from "./empty.js"
export {
assert,
describe,
expect,
expectToFail
} from "./test/exports.js"

export type { empty } from "./empty/exports.js"
export type { nonempty } from "./nonempty/exports.js"
export type { any } from "./any/exports.js"
export type { array, nonemptyArray, queue, tuple } from "./array/exports.js"
export type { some } from "./some.js"
Expand All @@ -23,8 +24,10 @@ export type {
Msg,
Partial,
partial,
TypeError
} from "./err/exports.js"

export type { TypeError } from "./type-error/exports.js"

export type {
bigint,
int,
Expand Down
2 changes: 1 addition & 1 deletion src/kind-new/kind.spec.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import type { any } from "../any/exports.js"
import type { _ } from "../util.js"
import type { nonempty } from "../empty.js"
import type { nonempty } from "../nonempty/nonempty.js"
import type { never } from "../never/exports.js"
import type { assert, expect } from "../test/exports.js"

Expand Down
2 changes: 1 addition & 1 deletion src/kind/kind.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import type { any } from "../any/exports.js"
import type { _ } from "../util.js"
import type { Err } from "../err/exports.js"
import type { Union } from "../union/exports.js"
import type { nonempty } from "../empty.js"
import type { nonempty } from "../nonempty/nonempty.js"

declare namespace kind {
/** namespace exports */
Expand Down
2 changes: 1 addition & 1 deletion src/lens/focus.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ export type {

import { describe, empty, expect, never } from "../exports.js"
import type { any } from "../any/exports.js"
import type { nonempty } from "../empty.js"
import type { nonempty } from "../nonempty/nonempty.js"

declare namespace List {
type head<type extends any.array> = type[0]
Expand Down
2 changes: 1 addition & 1 deletion src/lens/monocle.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ export type { named }

import type { some } from "../some.js"
import type { any } from "../any/exports.js"
import type { nonempty } from "../empty.js"
import type { nonempty } from "../nonempty/nonempty.js"
import type { never } from "../exports.js"

type pick<props extends any.array<any.keyof<type>>, type> = never | ({ [prop in props[number]]: type[prop] })
Expand Down
Loading

0 comments on commit cdf6897

Please sign in to comment.