Skip to content

Commit

Permalink
upup
Browse files Browse the repository at this point in the history
  • Loading branch information
ssalbdivad committed Jan 29, 2025
1 parent 5e1b707 commit c1cba0c
Show file tree
Hide file tree
Showing 9 changed files with 66 additions and 50 deletions.
28 changes: 19 additions & 9 deletions ark/docs/content/docs/expressions/index.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -141,7 +141,7 @@ const sameError = type({ a: "string.numeric.parse" }).or({ b: "string.numeric.pa
```

<details>
<summary>Learn the basic set theory behind this restriction</summary>
<summary>Learn the set theory behind this restriction</summary>

If you're relatively new to set-based types, that error might be daunting, but if you take a second to think through the example, it becomes clear why this isn't allowed. The logic of `bad` is essentially:

Expand All @@ -168,27 +168,37 @@ Add a type-only symbol to an existing type so that the only values that satisfy
<SyntaxTab string>

```ts
// hover to see ArkType's representation of a branded type
const myObject = type({
brandedString: "string#id"
})
// @noErrors
const even = type("(number % 2)#even")
type Even = typeof even.infer

const good: Even = even.assert(2)
// TypeScript: Type 'number' is not assignable to type 'Brand<number, "even">'
const bad: Even = 5
```

</SyntaxTab>

<SyntaxTab fluent>

```ts
// hover to see ArkType's representation of a branded type
const myObject = type({
brandedString: type.string.brand("id")
})
// @noErrors
const even = type.number.divisibleBy(2).brand("even")
type Even = typeof even.infer

const good: Even = even.assert(2)
// TypeScript: Type 'number' is not assignable to type 'Brand<number, "even">'
const bad: Even = 5
```

</SyntaxTab>

</SyntaxTabs>

Brands can be a great way to represent constraints that fall outside the scope TypeScript, but remember they don't change anything about what is enforced at runtime!

For more information on branding in general, check out [this excellent article](https://www.learningtypescript.com/articles/branded-types) from [Josh Goldberg](https://github.com/joshuakgoldberg).

### Narrow

Narrow expressions allow you to add custom validation logic and error messages. You can read more about them in [their intro section](/docs/intro/adding-constraints#narrow).
Expand Down
2 changes: 1 addition & 1 deletion ark/repo/scratch.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
import { type } from "arktype"

type("string")
const even = type.number.divisibleBy(2).brand("even")
11 changes: 11 additions & 0 deletions ark/type/__tests__/brand.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -70,4 +70,15 @@ contextualize(() => {
attest(string.json).equals(fluent.json)
attest<typeof fluent.t>(string.t)
})

it("docs example", () => {
const fluent = type.number.divisibleBy(2).brand("even")

attest<Brand<number, "even">>(fluent.t)
attest<number>(fluent.inferIn)
attest<Brand<number, "even">>(fluent.infer)

const string = type("(number % 2)#even")
attest<typeof fluent>(string).is(fluent)
})
})
56 changes: 28 additions & 28 deletions ark/type/__tests__/match.test.ts
Original file line number Diff line number Diff line change
@@ -1,31 +1,31 @@
import { attest } from "@ark/attest"
import { match } from "arktype"

it("cases only", () => {
const sizeOf = match({
"string|Array": v => v.length,
number: v => v,
bigint: v => v
}).orThrow()

attest<number>(sizeOf("abc")).equals(3)
attest<number>(sizeOf([1, 2, 3])).equals(3)
attest<bigint>(sizeOf(5n)).equals(5n)
})

it("properly infers types of inputs/outputs", () => {
const matcher = match({ string: s => s, number: n => n })
.when("boolean", b => b)
.orThrow()

// properly infers the type of the output based on the input
attest<string>(matcher("abc")).equals("abc")
attest<number>(matcher(4)).equals(4)
attest<boolean>(matcher(true)).equals(true)

// and properly handles unions in the input type
attest<string | number>(matcher(0 as string | number))
})
// import { attest } from "@ark/attest"
// import { match } from "arktype"

// it("cases only", () => {
// const sizeOf = match({
// "string|Array": v => v.length,
// number: v => v,
// bigint: v => v
// }).orThrow()

// attest<number>(sizeOf("abc")).equals(3)
// attest<number>(sizeOf([1, 2, 3])).equals(3)
// attest<bigint>(sizeOf(5n)).equals(5n)
// })

// it("properly infers types of inputs/outputs", () => {
// const matcher = match({ string: s => s, number: n => n })
// .when("boolean", b => b)
// .orThrow()

// // properly infers the type of the output based on the input
// attest<string>(matcher("abc")).equals("abc")
// attest<number>(matcher(4)).equals(4)
// attest<boolean>(matcher(true)).equals(true)

// // and properly handles unions in the input type
// attest<string | number>(matcher(0 as string | number))
// })

// describe('"finalizations"', () => {
// it(".orThrow()", () => {
Expand Down
1 change: 0 additions & 1 deletion ark/type/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,6 @@ export {
define,
generic,
keywords,
match,
type,
type Ark
} from "./keywords/keywords.ts"
Expand Down
3 changes: 1 addition & 2 deletions ark/type/keywords/keywords.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ import type { ArkErrors, arkKind } from "@ark/schema"
import type { Brand, inferred } from "@ark/util"
import type { distill, InferredMorph, Out, To } from "../attributes.ts"
import type { GenericParser } from "../generic.ts"
import type { MatchParser } from "../match.ts"
import type { BaseType } from "../methods/base.ts"
import type { BoundModule, Module } from "../module.ts"
import type {
Expand Down Expand Up @@ -129,7 +128,7 @@ export declare namespace type {

export type type<t = unknown, $ = {}> = Type<t, $>

export const match: MatchParser<{}> = ark.match as never
// export const match: MatchParser<{}> = ark.match as never

export const generic: GenericParser<{}> = ark.generic as never

Expand Down
4 changes: 2 additions & 2 deletions ark/type/scope.ts
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ import {
type parseValidGenericParams
} from "./generic.ts"
import type { Ark, type } from "./keywords/keywords.ts"
import { InternalMatchParser, type MatchParser } from "./match.ts"
import { InternalMatchParser } from "./match.ts"
import type {
BoundModule,
Module,
Expand Down Expand Up @@ -362,7 +362,7 @@ export interface Scope<$ = {}> {

type: TypeParser<$>

match: MatchParser<$>
// match: MatchParser<$>

declare: DeclarationParser<$>

Expand Down
4 changes: 2 additions & 2 deletions ark/util/errors.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import type { keyNonimal } from "./generics.ts"
import type { brand } from "./generics.ts"

export class InternalArktypeError extends Error {}

Expand Down Expand Up @@ -48,7 +48,7 @@ export interface ErrorType<
message extends string = string,
ctx extends {} = {}
> {
[keyNonimal]: "ErrorObject"
[brand]: "ErrorObject"
message: message
ctx: ctx
}
Expand Down
7 changes: 2 additions & 5 deletions ark/util/generics.ts
Original file line number Diff line number Diff line change
Expand Up @@ -64,13 +64,10 @@ export type exactEquals<l, r> =
(<_>() => _ extends l ? 1 : 2) extends <_>() => _ extends r ? 1 : 2 ? true
: false

/** You can avoid suggesting completions by prefixing a string key with whitespace.
* Isn't that keyNominal?
*/
export const keyNonimal = " keyNonimal"
export const brand = noSuggest("brand")

export type Brand<t = unknown, id = unknown> = t & {
readonly [keyNonimal]: [t, id]
readonly [brand]: [t, id]
}

export type unbrand<t> = t extends Brand<infer base, string> ? base : never
Expand Down

0 comments on commit c1cba0c

Please sign in to comment.