Skip to content

Commit

Permalink
docs(website): improve typescript docs for operators
Browse files Browse the repository at this point in the history
  • Loading branch information
christianalfoni committed Jan 16, 2019
1 parent 1f53b60 commit b4d4adb
Show file tree
Hide file tree
Showing 12 changed files with 303 additions and 72 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
const javascript = {
react: [
{
fileName: 'components/MyComponent.jsx',
target: 'jsx',
code: `
import React from 'react'
import { connect } from '../overmind'
const MyComponent = ({ overmind }) => (
<button onClick={overmind.actions.functionalAction}>
Test
</button>
)
export default connect(MyComponent)
`,
},
],
vue: [
{
fileName: 'components/MyComponent.vue (template)',
target: 'markup',
code: `
<button on:click="overmind.actions.functionalAction()">
Test
</button>
`,
},
{
fileName: 'components/MyComponent.vue (script)',
code: `
import { connect } from '../overmind'
export default connect({})
`,
},
],
}

const typescript = {
react: [
{
fileName: 'components/MyComponent.tsx',
code: `
import * as React from 'react'
import { connect, Connect } from '../overmind'
type Props = Connect
const MyComponent: React.FunctionComponent<Props> = ({ overmind }) => (
<button onClick={overmind.actions.functionalAction}>
Test
</button>
)
export default connect(MyComponent)
`,
},
],
vue: [
{
fileName: 'components/MyComponent.vue (template)',
target: 'markup',
code: `
<button on:click="overmind.actions.functionalAction()">
Test
</button>
`,
},
{
fileName: 'components/MyComponent.vue (script)',
code: `
import { connect } from '../overmind'
export default connect({})
`,
},
],
angular: [
{
fileName: 'components/grabdata.component.ts',
code: `
import { Component,Input } from '@angular/core';
import { connect } from '../overmind'
@Component({
selector: 'grab-data',
template: \`
<button (click)="overmind.actions.functionalAction()">
Test
</button>
\`
})
@connect()
export class GrabData {}
`,
},
],
}

export default (ts, view) => (ts ? typescript[view] : javascript[view])
39 changes: 20 additions & 19 deletions packages/overmind-website/examples/guide/goingfunctional/clean.ts
Original file line number Diff line number Diff line change
@@ -1,23 +1,11 @@
export default (ts) =>
ts
? [
{
fileName: 'overmind/operators.ts',
code: `
import { Operator, map, filter } from 'overmind'
const getEventTargetValue: Operator<Event, string> =
map(({ value }) => value.currentTarget.value)
const lengthGreaterThan: (length: number) => Operator<string> =
(length) => filter(({ value }) => value.length > length)
`,
},
{
fileName: 'overmind/actions.ts',
code: `
import { Operator, pipe, debounce, action } from 'overmind'
import { getEventTargetValue, lengthGreaterThan } from './operators'
export const search: Operator<Event> = pipe(
getEventTargetValue,
Expand All @@ -31,18 +19,21 @@ export const search: Operator<Event> = pipe(
)
`,
},
]
: [
{
fileName: 'overmind/operators.js',
fileName: 'overmind/operators.ts',
code: `
import { map, filter } from 'overmind'
import { Operator, map, filter } from 'overmind'
export const getEventTargetValue = map(({ value }) => value.currentTarget.value)
const getEventTargetValue: Operator<Event, string> =
map(({ value }) => value.currentTarget.value)
export const lengthGreaterThan = (length) => filter(({ value }) => value.length > length)
const lengthGreaterThan: (length: number) => Operator<string> =
(length) => filter(({ value }) => value.length > length)
`,
},
]
: [
{
fileName: 'overmind/actions.js',
code: `
Expand All @@ -61,4 +52,14 @@ export const search = pipe(
)
`,
},
{
fileName: 'overmind/operators.js',
code: `
import { map, filter } from 'overmind'
export const getEventTargetValue = map(({ value }) => value.currentTarget.value)
export const lengthGreaterThan = (length) => filter(({ value }) => value.length > length)
`,
},
]
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
export default (ts) =>
ts
? [
{
fileName: 'overmind/operators.ts',
code: `
import { Operator, filter } from 'overmind'
const lengthGreaterThan: (length: number) => Operator<string> =
(length) => filter(({ value }) => value.length > length)
`,
},
]
: [
{
fileName: 'overmind/operators.js',
code: `
import { map, filter } from 'overmind'
export const lengthGreaterThan = (length) => filter(({ value }) => value.length > length)
`,
},
]
15 changes: 15 additions & 0 deletions packages/overmind-website/examples/guide/typescript/action.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
export default () => [
{
code: `
import { Action } from 'overmind'
export const noArgAction: Action = ({ value }) => {
value // this becomes "void"
}
export const argAction: Action<string> = ({ value }) => {
value // this becomes "string"
}
`,
},
]
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
export default () => [
{
fileName: 'overmind/operators.ts',
code: `
import { Operator, action } from 'overmind'
export const doSomeStateChange: <T>() => Operator<T> =
() => action(({ state }) => {
state.foo = 'bar'
})
`,
},
{
fileName: 'overmind/actions.ts',
code: `
import { Operator, pipe, action } from 'overmind'
import { doSomeStateChange } from './operators'
export const setInput: Operator<string> = pipe(
doSomeStateChange<string>(),
action(({ value: input, state }) => {
state.input = input
})
)
`,
},
]
Original file line number Diff line number Diff line change
Expand Up @@ -4,19 +4,21 @@ export default () => [
code: `
import { Operator, filter } from 'overmind'
export const filterAwesomeUser: Operator<{ isAwesome: boolean }> =
export const filterAwesome: Operator<{ isAwesome: boolean }> =
filter(({ value: somethingAwesome }) => somethingAwesome.isAwesome)
`,
},
{
fileName: 'overmind/actions.ts',
code: `
import { Operator, pipe, action } from 'overmind'
import { filterAwesomeUser } from './operators'
import { filterAwesome } from './operators'
import { User } from './state'
export const clickedUser: Operator<User> = pipe(
filterAwesomeUser,
// We get an error here, because this operator explicitly
// outputs the type { isAwesome: boolean }
filterAwesome,
action(({ value: user, state }) => {
state.awesomeUsersClickedCount++
})
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
export default () => [
{
fileName: 'overmind/operators.ts',
code: `
import { Operator, filter } from 'overmind'
export const filterAwesome: <T>() => Operator<{ isAwesome: boolean }, T> =
() => filter(({ value: somethingAwesome }) => somethingAwesome.isAwesome)
`,
},
{
fileName: 'overmind/actions.ts',
code: `
import { Operator, pipe, action } from 'overmind'
import { filterAwesome } from './operators'
import { User } from './state'
export const clickedUser: Operator<User> = pipe(
filterAwesome<User>(),
action(({ value: user, state }) => {
state.awesomeUsersClickedCount++
})
)
`,
},
]
72 changes: 69 additions & 3 deletions packages/overmind-website/guides/intermediate/03_typescript.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

Overmind is written in Typescript and it is written with a focus on your keeping as little time as possible helping Typescript understand what your app is all about. Typescript will spend a lot more time helping you. There are actually two approaches to typing in Overmind.

## 1. Declare module
## Declare module

The most straight forward way to type your application is to use the **declare module** approach. This will work for most applications, but might make you feel uncomfortable as a harcore Typescripter. The reason is that we are overriding an internal type, meaning that you can only have one instance of Overmind running inside your application.

Expand All @@ -16,9 +16,75 @@ Now you can import any type directly from Overmind and it will understand the co
h(Example, { name: "guide/typescript/declare_imports.ts" })
```

## 2. Explicit typing
## Explicit typing
You can also explicitly type your application. This gives more flexibility.

```marksy
h(Example, { name: "guide/typescript/explicit.ts" })
```
```

## Actions

The action type takes either no arguments or a single argument. If you give no arguments to the action it will be typed as not expecting an argument at all. When you do type with an argument that is the type of the **value** on the context. This value is populated when you call the action on the Overmind instance.

```marksy
h(Example, { name: "guide/typescript/action.ts" })
```


## Operators

Operators is like the **Action** type, it can take an optional input, but it can also have an output. By default the output of an operator is the same as the input. Most operators has its output the same as the input, meaning the incoming values just passes through.

```marksy
h(Example, { name: "guide/typescript/operatorinputsandoutputs" })
```

Now what is important to understand is that the **Operator** is used with all operators in Overmind, cause all of them is about "passing a value to the next operator". The arguments you give has to match the specific operator though. So for example if you type an **action** operator with a different output than the input:

```marksy
h(Example, { name: "guide/typescript/wrongoperator" })
```

Typescript yells at you, because this operator just passes the value straight through.

Typically you do not think about this and Typescript rather yells at you when the value you are passing through your operators is not matching up.

### Caveats
There are two **limitations** to the operators type system which we are still trying to figure out:

#### 1. Partial input type

For example:

```marksy
h(Example, { name: "guide/typescript/operatorpartial" })
```

There is no way to express in Typescript that you should uphold this partial typing of **filterAwesomeUser** and still pass the inferred input, **User**, as the output. This will give a typing error.

This can be handled by making the operator a factory instead:

```marksy
h(Example, { name: "guide/typescript/operatorpartial_solution" })
```

#### 2. Infer input

You might create an operator that does not care about its input. For example:

```marksy
h(Example, { name: "guide/typescript/operatorinfer" })
```

Composing **doSomeStateChange** into the **pipe** gives an error, cause this operator expects a **void** type. There is no other way to make this work than inlining the **doSomeStateChange** operator into the pipe in question, which infers the correct input.

You could use the same trick here though:

```marksy
h(Example, { name: "guide/typescript/operatorinfer_solution" })
```


#### Summary
Typescript is not functional, it is object oriented and it is very difficult to make it work with this kind of APIs. We have high hopes though cause Typescript is evolving rapidly and Microsoft is dedicated to make this an awesome type system for JavaScript. If you got mad Typescript skills, please contact us to iterate on the type system and make this stuff work :-)
Loading

0 comments on commit b4d4adb

Please sign in to comment.