schotime / CerebralTS

MIT License
3 stars 0 forks source link

Discussion summary #1

Open christianalfoni opened 7 years ago

christianalfoni commented 7 years ago

Thanks for a super interesting talk on TypeScript support and the work you have done @schotime!

The talk is here: https://www.youtube.com/watch?v=xMUxCH5uCWY

Here is a short summary, note that API is just temporary to explore, this will be part of further discussions:

interface ExtProps {
  itemId: string
}

connect<ExtProps>()
  .with(({state, props, signal}) => ({
    // All gives suggestions and errors if wrong usage
    foo: state(s => s.foo),
    // Dynamic
    item: state(s => s.items[0], props(p => p.itemId)),
    // Signal
    clicked: signal(s => s.clicked)
  })
  .to(function Comp (props) {
    props.foo // Understands what props are actually here
  })
chainWithNoInput(x => x
  .seq(addItem)
  .parallel(p => p
    .seq(async ({ props, http }) => {
      let results = await http.get<any>("https://api.flickr.com/services/rest/?&method=flickr.people.getPublicPhotos&format=json&api_key=6f93d9bd5fef5831ec592f0b527fdeff&user_id=9395899@N08");
      return { flickies: results.result }
    })
    .seq(async ({ props, http }) => {
      let results = await http.get<Countries[]>("https://restcountries.eu/rest/v2/regionalbloc/eu");
      return { countries: results.result[0].flag }
    })
  )
  .seq(({ props, helper }) => { console.log("ehh?", props, helper); })
  .seqPath(pathTest)
  .withPaths({
    success: y => y.seq(({ props }) => { console.log("success", props.newTitleResult) }),
    error: y => y.seq(({ props }) => { console.log("error", props.newTitleResult) })
  })
);

Signals understands their payloads, outputs from actions, paths necessary etc. Basically you get a ton of help making sure you are doing it right.

function addItem({ props, helper, http }: MyContext<{}>): Output {
  helper.state(x => x.items).unshift(helper.state(x => x.newItemTitle).get());

  return {
    newTitle: helper.state(x => x.newItemTitle).get()
  }
}

I will make a comment on suggested progress to iterate on this work. Thanks a bunch again @schotime , this is very awesome stuff :)

christianalfoni commented 7 years ago

Okay, so this is what I suggest:

Module constructor

We have already talked about changing the signature of Controller to:

Controller(rootModule, options)

That means the controller is basically a module with the same properties as any other module. It makes things a lot more consistent. In addition to this I want to give modules a constructor:

Module({
  state: {},
  signals: {},
  catch: new Map([]),
  providers: [],
  modules: {}
})

// Alternatively
Module(details => ({
  state: {},
  signals: {},
  catch: new Map([]),
  providers: [],
  modules: {}
}))

This will allow us to give type definitions to modules and open up for actually defining state and signals as modules with Typescript, as I understand it @schotime ?

Components

I suggest changing the api to:

interface ExtProps {
  itemId: string
}

connect<ExtProps>(({state, props, signal}) => ({
    foo: state(s => s.foo),
  })(  // <- Is this possible? Or change `to` to `component`
    function Comp (props) {
      props.foo // Understands what props are actually here
    }
  )

Signals

interface Props {
  title: string
}

// Can also start with parallel
sequence<Props>(s => s
  .action(addItem)
  .parallel(p => p
    .action(someAction)
    .action(someOtherAction)
  )
  .actionWithPaths(pathTest)
  .paths({
    success: s => s.action(actionA),
    error: s => s.action(actionB)
  })
  .sequence(s => s) // Add a sequence to a sequence
);

Actions

function addItem({ props, state, http }: MyContext<{}>): Output {
  state(s => s.items).push(props.item);

  return {
    newTitle: state(s => s.newItemTitle).get()
  }
}
mrvelic commented 7 years ago

Modules

@christianalfoni with the proposal for a module constructor, would you be inferring state model nesting via the nesting of modules? Kind of like...

{
  moduleName: {
    subModuleName: {
      someProp: []
    },
    items: []
  },
  anotherModule: {}
}

At the moment if you want to use modules, it's a matter of building a signals and state model interface which represents your hierarchy of modules and their state, so you can define interfaces for each module and then import them into your root model to provide the layout.

// pretend these were imported from elsewhere
interface Module1StateModel {
  items: string[]
}
interface Module2StateModel {
  stuff: number[]
}

interface StateModel {
  module1Name: Module1StateModel,
  module2Name: Module2StateModel,
  someRootStateItem: string
}

Then your controller defined like TSController<StateModel, {}>({ modules: { module1Name: module1, module2Name: module2} }) which would give you state tag lambdas like state(x => x.module1Name.items)

Components

I think the change in calling the connect makes sense, the only challenge may be providing state and signal to the inner parameters of the mapping lambda since this requires connect to have knowledge of your state model, meaning you would have to define your own (maybe in helpers). We could provide a template function that can be retyped and exported with your concrete model in the cerebral-ts/react package though.

Actions

The change to actions could be implemented by having the action context provider factory create the state and other strongly typed tag properties on the context object. We were a bit wary of doing this since we didn't want to interfere to much with other established conventions.

Signals

I think the changes to the naming of the chain building makes sense since you're wanting to place more emphasis on the sequence idea, and "action" does read better.

mrvelic commented 7 years ago

So @schotime and I just tested out implementing the change to the connect calling method, unfortunately turning it into a connect(map =>({})(component)) doesn't work with typescript since you cant infer a return type and return a function which uses that generic type at the same time, since the outer function must return the type you're expecting which will never be a function. You could do something like connect<ExtProps>()(map)(component) but you're getting diminishing gains at that point and it still reads better when intellisense gives you the fluent interface as you're working. Therefore I think we should stick with connect<EP>().from(map => ({})).to(component) as a pattern, but we could give "from" and "to" better names if its deemed appropriate. Intellisense will tell you that you are going "from" a map, and "to" a component.

schotime commented 7 years ago
interface ExtProps {
  itemId: string
}

connect<ExtProps>()
  .with(({state, props, signal}) => ({
    // All gives suggestions and errors if wrong usage
    foo: state(s => s.foo),
    // Dynamic
    item: state(s => s.items[0], props(p => p.itemId)),
    // Signal
    clicked: signal(s => s.clicked)
  })
  .to(function Comp (props) {
    props.foo // Understands what props are actually here
  })
christianalfoni commented 7 years ago

@mrvelic @schotime Thanks for the feedback :)

If I understand you correctly there is no reason to change the module definition (other than type definitions for the IDE). To use modules to nest state and signals it is rather about extracting the type definitions from the modules? Kinda like Elm does it?

So, as you say:

import {
  StateModel as Module1StateModel,
  SignalsModel as Module1SignalsModel
} from './modules/module1'

import {
  StateModel as Module2StateModel,
  SignalsModel as Module2SignalsModel
} from './modules/module2'

interface StateModel {
  module1Name: Module1StateModel,
  module2Name: Module2StateModel
}

interface SignalsModel {
  module1Name: Module1SignalsModel,
  module2Name: Module2SignalsModel
}

?

schotime commented 7 years ago

That's exactly it!!