Skip to content

Instantly share code, notes, and snippets.

@micimize
Last active November 29, 2019 16:43
Show Gist options
  • Save micimize/9964338e0e052d1b15c821835c75db1d to your computer and use it in GitHub Desktop.
Save micimize/9964338e0e052d1b15c821835c75db1d to your computer and use it in GitHub Desktop.
[DON'T USE, JUST SCRATCH] Old Fantasy land / io-ts / immutable.js ideas
import * as I from 'immutable'
import { l, t, validater } from './t'
function IOMap<K extends t.Any, V extends t.Any>(keyType: K, valueType: V) {
type Key = t.TypeOf<K>
type Value = t.TypeOf<V>
type M = I.Map<Key, Value>
const Entries = t.array(t.tuple([ keyType, valueType ]))
type Entries = t.TypeOf<typeof Entries>
type Type = t.Type<M> & {
of: (value: Entries) => M
}
const Type: Type = {
_A: t._A,
name: `Map<${keyType.name}, ${valueType.name}>'`,
validate: (value: object, context) =>
Entries.validate(Object.entries(value), context)
.chain(entries => t.success(I.Map<Key, Value>(entries))),
of: (entries: Entries): M => I.Map<Key, Value>(entries)
}
return Type //{ Map, Type }
}
namespace IOMap {
export const of = I.Map.of
export const isMap = I.Map.of
}
export default IOMap
import * as I from 'immutable'
import { l, t, validater } from './t'
type Defaults<P extends t.Props> = t.PartialOf<P>
function emptyDefaults<P extends t.Props>(props: P): Defaults<P> {
let o = {}
Object.keys(props).forEach(k =>
o[k] = undefined)
return o as Defaults<P>
}
// Prefilled Records don't require full or any arguments, as the defaults already satisfy them
function PrefilledRecord<P extends t.Props>(props: P, defaults: t.TypeOf<typeof FullInterface>){
let FullInterface = t.interface(props)
type Interface = t.TypeOf<typeof FullInterface>
let R = I.Record(defaults as Object)
class PrefilledRecord extends R {
constructor(record: Partial<Interface> = defaults) {
super(record)
}
static of(record: Partial<Interface> = defaults): PrefilledRecord {
return new PrefilledRecord(record)
}
}
return PrefilledRecord
}
function Record<P extends t.Props>(props: P, defaults: Defaults<P> = emptyDefaults(props)){
let FullInterface = t.readonly(t.interface(props))
type Interface = t.TypeOf<typeof FullInterface>
let D = t.partial(props)
let validate = validater<Interface>(FullInterface)
class StrictRecord extends I.Record(defaults as object) {
constructor(record: Interface) {
super(validate(record))
}
static of(record: Interface): StrictRecord {
return new StrictRecord(record)
}
public of = (record: Interface) => StrictRecord.of(record)
}
return StrictRecord
}
export { Record, PrefilledRecord }
@micimize
Copy link
Author

I found these in some corner of my fs and thought I'd throw them in a gist. They're probably broken and I wouldn't recommend using them.

@GabrielCTroia
Copy link

Hey @micimize, these are pretty awesome. I was just thinking of creating an abstraction similar to this, so thank you for the inspiration. Care to show an example or 2 on how did you end up using them?

Also, do you think it's valuable/required to use Immutable and io-ts together?

Thank you

@micimize
Copy link
Author

@GabrielCTroia glad to hear its of some use! After some excavation, Here's what I believe is a usage example:

const Context = Record({
  innovation: t.Integer,
  activations: InnovationMap(ActivationRef),
  nodes: InnovationMap(PotentialNode),
  connections: InnovationMap(PotentialConnection)
}, {
  innovation: 2,
  activations: { 0: 'INPUT', 1: 'BIAS' },
  nodes: {},
  connections: {}
})

let context = t.validate({ foo: 'bar' }, Context)

I abandoned this approach on live-neat, but there might be other usage examples in the tree at that commit. I think I just felt like I was getting distracted from the main focus of the project

@GabrielCTroia
Copy link

GabrielCTroia commented Oct 15, 2019

That's pretty cool! Thank you!

I ended up with something a bit different – also I ditched Immutable for the simplicity of the Readonly Type, b/c I believe compile time immutable should be good enough.

Inspired by your idea, this is what I ended up with – still a work in progress as there are some things that are a bit quarky, like the exported types (RecordType, CollectionType), but I thing this API is a bit friendlier than the io-ts default.

/**
 * Factory method for creating Opinioated Payload Records
 *  - Readonly by default
 *  - Exact Types - all the extra props are removed
 *  - io is provided in the callback to avoid import pollution
 *  - Out of the box support for Records & Collections
 *
 * @param name
 * @param requiredFn
 * @param optionalsFn
 */
export const RecordFactory = <TRequired extends io.Props, TOptional extends io.Props>(
  name: string,
  requiredFn: (t: typeof enhancedIO) => io.TypeC<TRequired>,
  optionalsFn: (t: typeof enhancedIO) => io.PartialC<TOptional> = () => io.partial({} as TOptional),
) => {
  const required = requiredFn(enhancedIO);
  const optional = optionalsFn(enhancedIO);

  const recordValidator = io.exact(io.intersection([required, optional]), name);
  type RecordType = DeepReadonly<io.TypeOf<typeof recordValidator>>;


  function recordOf(payload: unknown) {
    const decoded = recordValidator.decode(payload);

    if (isLeft(decoded)) {
      const messages = reporter(decoded);

      console.error(messages.join('\n'));
      console.log('Faulty Payload', payload);
      throw new Error('RecordOf Validation Error!');
    }

    return decoded.right as RecordType;
  }

  const collectionValidator = io.array(recordValidator, `${name}Collection`);
  type CollectionType = DeepReadonly<io.TypeOf<typeof collectionValidator>>;

  function collectionOf(payload: unknown) {
    const decoded = collectionValidator.decode(payload);

    if (isLeft(decoded)) {
      const messages = reporter(decoded);

      console.error(messages.join('\n'));
      console.log('Faulty Payload', payload);
      throw new Error('CollectionOf Validation Error!');
    }

    return decoded.right as CollectionType;
  }


  return {
    // Use these to create records from paloads
    recordOf,
    collectionOf,

    // Use these to create composite records
    recordValidator,
    collectionValidator,

    // DO NOT USE THESE AS VALUES! But only to get the Type, not as a Value
    // type MyRecord = typeof Record.RecordType
    get RecordType(): RecordType {
      throw new Error('RecordFactory - attempted to use RecordType as Value. Did you mean `typeof RecordType`?');
    },
    get CollectionType(): CollectionType {
      throw new Error('RecordFactory - attempted to use CollectionType as Value. Did you mean `typeof CollectionType`?');
    },
  };
};

And you use it like:

import { RecordFactory } from 'src/lib/recordFactory';


export const Vehicle = RecordFactory(
  'Vehicle',
  (t) => t.type({
    id: t.StringFromNumber,
    userId: t.StringFromNumber,
    make: t.string,
    images: t.array(
      t.type({
        imageUrl: t.string,
        mobileUrl: t.string,
      }),
    ),
    averageRating: t.NumberFromAny,
    miles: t.NumberFromAny,
  }),
);

export type VehicleRecord = typeof Vehicle.RecordType;
export type VehicleCollection = typeof Vehicle.CollectionType;

Your opinion would be greatly appreciated! Thanks

@micimize
Copy link
Author

@GabrielCTroia Looks like a nice API to me! I think the main thing I would miss from my API would be the default object param

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment