Skip to content

Commit

Permalink
Add lift
Browse files Browse the repository at this point in the history
  • Loading branch information
JAForbes committed Feb 13, 2024
1 parent 1a150a3 commit 93f1ebe
Show file tree
Hide file tree
Showing 5 changed files with 92 additions and 127 deletions.
100 changes: 0 additions & 100 deletions lib/index.js

This file was deleted.

35 changes: 18 additions & 17 deletions lib/index.ts
Expand Up @@ -22,6 +22,9 @@ export type CoreAPI<N extends string, D extends Definition> = {
instance: InternalInstance<N, D, keyof D>,
options: MatchOptions<D, T>,
) => T
lift: <T>(
fn: (x: InternalInstance<N, D, keyof D>) => T,
) => (x: InternalInstance<N, D, keyof D>) => T
}
export type API<N extends string, D extends Definition> = CoreAPI<N, D> &
Constructors<N, D> &
Expand Down Expand Up @@ -85,16 +88,9 @@ export type EitherApi<Name extends string, Yes, No> = API<
// 'Y' | 'N'
// >

map: API<
Name,
{ Y: (_: Yes) => any; N: (_: No) => any }
>["mapY"]

map: API<Name, { Y: (_: Yes) => any; N: (_: No) => any }>['mapY']

flatMap: API<
Name,
{ Y: (_: Yes) => any; N: (_: No) => any }
>["flatMapY"]
flatMap: API<Name, { Y: (_: Yes) => any; N: (_: No) => any }>['flatMapY']
}

type InternalInstance<
Expand All @@ -107,6 +103,10 @@ function match(instance: any, options: any): any {
return options[instance.tag](instance.value)
}

function lift(fn: any): any {
return (x: any) => fn(x)
}

function otherwise(tags: string[]) {
return (fn: any) => Object.fromEntries(tags.map((tag) => [tag, fn]))
}
Expand All @@ -122,6 +122,7 @@ export function type<N extends string, D extends Definition>(
patterns: {},
definition,
match,
lift,
otherwise: (tagNames = tags) => otherwise(tagNames),
}

Expand Down Expand Up @@ -189,14 +190,16 @@ export type UseMap<N extends string, D extends Definition> = {
[Key in keyof D as MapTemplate<Key extends string ? Key : never>]: <T>(
value: InternalInstance<N, D, keyof D>,
visitor: (value: InternalValue<D[Key]>) => T,
) => Instance<API<N, Omit<D, Key> & Record<Key,(_:T) => any>>>
) => Instance<API<N, Omit<D, Key> & Record<Key, (_: T) => any>>>
}

export type UseFlatMap<N extends string, D extends Definition> = {
[Key in keyof D as FlatMapTemplate<Key extends string ? Key : never>]: <T>(
value: InternalInstance<N, D, keyof D>,
visitor: (value: InternalValue<D[Key]>) => Instance<API<N, Omit<D, Key> & Record<Key,(_:T) => any>>>,
) => Instance<API<N, Omit<D, Key> & Record<Key,(_:T) => any>>>
visitor: (
value: InternalValue<D[Key]>,
) => Instance<API<N, Omit<D, Key> & Record<Key, (_: T) => any>>>,
) => Instance<API<N, Omit<D, Key> & Record<Key, (_: T) => any>>>
}

export type UseGet<N extends string, D extends Definition> = {
Expand All @@ -211,13 +214,13 @@ export type UseGetDefault<N extends string, D extends Definition> = {
[Key in keyof D as GetTemplate<Key extends string ? Key : never>]: <T>(
value: InternalInstance<N, D, keyof D>,
fallback: T,
) => T | InternalInstance<N, D, keyof D>["value"]
) => T | InternalInstance<N, D, keyof D>['value']
}

export type UseGetNull<N extends string, D extends Definition> = {
[Key in keyof D as GetTemplate<Key extends string ? Key : never>]: (
value: InternalInstance<N, D, keyof D>,
) => null | InternalInstance<N, D, keyof D>["value"]
) => null | InternalInstance<N, D, keyof D>['value']
}

type InternalValue<I extends (v: any) => any> = Parameters<I>[0]
Expand Down Expand Up @@ -255,7 +258,6 @@ export function either<Name extends string, Yes, No>(
Y: yesFunction,
N: noFunction,
})

;(api as any).map = api.mapY
;(api as any).flatMap = api.flatMapY
return api as EitherApi<Name, Yes, No>
Expand All @@ -264,8 +266,7 @@ export function either<Name extends string, Yes, No>(
// alias
export { either as maybe }


export function Resource<Name extends string, Value extends any>(name: Name){
export function Resource<Name extends string, Value extends any>(name: Name) {
const Resource = type(name, {
Loading: (_: { progress?: number }) => _,
Loaded: (_: Value) => _,
Expand Down
4 changes: 2 additions & 2 deletions package.json
Expand Up @@ -8,10 +8,10 @@
"module": "./dist/sum-type.esm.js",
"unpkg": "./dist/sum-type.esm.js",
"scripts": {
"test": "npx tsc ./test/index.ts --noEmit --esModuleInterop && node --import tsx --test test/*.ts",
"test": "npx tsc ./test/index.ts --noEmit && node --import tsx --test test/*.ts",
"dev": "node --watch --import tsx --test test/*.ts",
"build:bundle": "esbuild lib/index.ts --bundle --format=esm --sourcemap --allow-overwrite --outfile=./dist/sum-type.esm.js",
"build:types": "npx tsc ./test/index.ts --noEmit && npx tsc",
"build:types": "npx tsc ./test/index.ts --noEmit --esModuleInterop && npx tsc",
"build:clear": "rm -fr ./dist",
"build": "npm run build:clear && npm run build:bundle && npm run build:types",
"prepublishOnly": "npm version prerelease --preid=next && npm run build"
Expand Down
62 changes: 55 additions & 7 deletions readme.md
Expand Up @@ -25,17 +25,17 @@ const loaded =
const loading =
Loaded.N(55)

const render = (x: T.Instance<typeof Loaded>) =>
const render = Loaded.lift( x =>
Loaded.bifold(
x
, x => `Loading: ${x}%`
, x => `Loaded: ${x}`
)
))

const transform = (x: T.Instance<typeof Loaded>) =>
const transform = Loaded.lift( x =>
Loaded.mapY(
x, x => x.toUpperCase()
)
))

assert.deepEqual(Loaded.mapN( loading, x => x+'%' ), {...Loaded.N(55), value: '55%' })

Expand Down Expand Up @@ -245,7 +245,7 @@ switch (instance.tag) {
return 2
}
'Loaded': {
3
return 3
}
}
```
Expand Down Expand Up @@ -386,7 +386,30 @@ instance.tag === 'Loaded' ? instance.value.title : 'No Title'
Resource.getLoaded(instance, 'No Title', x => x.title)
```

### `type.encase`
### `type.lift`

Useful for defining a reusable function that accepts an instance of your type as an argument. Note you can do this manually with type utilities like `Instance` but doing it this way is may be preferable as the type of the parameter is inferred for you.

The following two examples are equivalent

```typescript
const render = (x: T.Instance<typeof Loaded> ) =>
Loaded.bifold(
x
, x => `Loading: ${x}%`
, x => `Loaded: ${x}`
)

```

```typescript
const render = Loaded.lift( x =>
Loaded.bifold(
x
, x => `Loading: ${x}%`
, x => `Loaded: ${x}`
))
```

### `type.map[Tag]`

Expand Down Expand Up @@ -593,11 +616,36 @@ Like any other subtype you automatically get generated methods for each tag, e.g
### `Value`
Returns the type of the value of a supertype or subtype depending on what you pass in as a generic:
```typescript
type All = T.Value<typeof ExampleResource>
// { value: { id: string, title: string } } | { progress?: number } | {} | Error

type Single = T.Value<typeof ExampleResource.Loaded>
// { value: { id: string, title: string } }
```
### `Instance`
Returns the type of the instance of a supertype or subtype depending on what you pass in as a generic:
```typescript
type All = T.Instance<typeof ExampleResource>
// | { type: 'ExampleResource', tag: 'Loaded', value: { id: string, title: string } }
// | { type: 'ExampleResource', tag: 'Loading', value: { progress?: number } }
// | { type: 'ExampleResource', tag: 'Empty', value: {} }
// | { type: 'ExampleResource', tag: 'Error', value: Error }

type Single = T.Instance<typeof ExampleResource.Loaded>
// | { type: 'ExampleResource', tag: 'Loaded', value: { id: string, title: string } }
```
Can be useful for annotating custom functions. But note `type.lift` can be more convenient as the `Instance` type is automatically inferred for you.
## FAQ
### Why is Either's subtypes named `Y|N` instead of `Right|Left`
### Why are Either's subtypes named `Y|N` instead of `Right|Left`
It's a night short and sweet convention and while `Right|Left` allows you to model things that aren't necessarily failable operations, generally that is what they are used for so we name it accordingly.
Expand Down
18 changes: 17 additions & 1 deletion test/index.ts
@@ -1,5 +1,5 @@
import test from 'node:test'
import * as assert from 'node:assert'
import assert from 'node:assert'

Check failure on line 2 in test/index.ts

View workflow job for this annotation

GitHub Actions / test (20.x)

Module '"node:assert"' can only be default-imported using the 'esModuleInterop' flag

import * as T from '../lib/index.js'

Expand Down Expand Up @@ -388,3 +388,19 @@ test('readme', () => {
]
)
})


test('lift', () => {
type File = { file_id: string, file_url: string, file_ext: string, file_upname: string }

const FileResource = T.Resource<'FileResource', File>('FileResource')

const fn = FileResource.lift( x => {
return x.tag
})

const tag = fn(FileResource.Loading({ progress: 55 }))

assert.equal(tag, 'Loading')

})

0 comments on commit 93f1ebe

Please sign in to comment.