Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Ceiling Type / Strict Interface #84

Closed
phiresky opened this issue Nov 4, 2017 · 7 comments
Closed

Ceiling Type / Strict Interface #84

phiresky opened this issue Nov 4, 2017 · 7 comments

Comments

@phiresky
Copy link
Contributor

phiresky commented Nov 4, 2017

I think a type that specifies that only the given interface properties are allowed would be useful. Working Implementation:

export class CeilingType<RT extends t.InterfaceType<any>> implements t.Type<t.TypeOf<RT>> {
	readonly _tag: "CeilingType" = "CeilingType";
	readonly _A: t.TypeOf<RT>;
	readonly validate: t.Validate<t.TypeOf<RT>>;
	constructor(readonly type: RT, readonly name: string = `ceil(${type.name})`) {
		const shouldBeProps = Object.getOwnPropertyNames(type.props);
		this.validate = (v, c) =>
			type.validate(v, c).chain(obj => {
				const props = Object.getOwnPropertyNames(obj);
				if (props.length === shouldBeProps.length) return t.success(obj);
				for (const prop of props) {
					if (!shouldBeProps.includes(prop))
						return t.failure(obj[prop], [...c, { key: prop, type: t.never }]);
				}
				throw `Impossible? got [${props.join(", ")}], expected  [${shouldBeProps.join(", ")}]`;
			});
	}
}
export function strictInterface<P extends t.Props>(props: P, name?: string): CeilingType<t.InterfaceType<P>> {
	return new CeilingType(t.interface(props, name));
}

Not sure what it should be called, so far i came up with "ceiling type" or "strict interface".

This implementation only works for own properties and ignores symbols, and there might be some special handling for unenumerable properties needed.

@gcanti
Copy link
Owner

gcanti commented Nov 4, 2017

Note that TypeScript isn't strict about additional properties (except for literal objects).

let p1: Person = { name: 'Giulio', age: 44, foo: 'bar' } // error

declare const p2: { name: string, age: number, foo: string }

p1 = p2 // ok

Anyway you could define a custom combinator (basically it's a refinement of an interface)

function strict<T extends t.InterfaceType<any>>(type: T): t.RefinementType<T> {
  const len = Object.keys(type.props).length
  return t.refinement(type, o => Object.keys(o).length === len, `Strict<${type.name}>`)
}

Usage

const Person = t.interface({
  name: t.string,
  age: t.number
})

const StrictPerson = strict(Person)

import { PathReporter } from 'io-ts/lib/PathReporter'

const validation = t.validate({ name: 'Giulio', age: 44, foo: 'bar' }, StrictPerson)

console.log(PathReporter.report(validation))
// [ 'Invalid value {"name":"Giulio","age":44,"foo":"bar"} supplied to : Strict<{ name: string, age: number }>' ]

@phiresky
Copy link
Contributor Author

phiresky commented Nov 4, 2017

Note that TypeScript isn't strict about additional properties (except for literal objects).

I know, but this library also has t.Integer even though to TS it's the same as a floating point number.

Anyway you could define a custom combinator (basically it's a refinement of an interface)

Yeah, that's almost exactly what I already did in the original post, is it not? I just think it would be useful to have as a part of the library, because I already need it in two projects.

gcanti added a commit that referenced this issue Nov 5, 2017
@gcanti
Copy link
Owner

gcanti commented Nov 5, 2017

Yeah, that's almost exactly what I already did in the original post, is it not?

Well actually no, your proposed solution would

  • allow to detect a strict type programmatically, refinements are "opaque"
  • return better error messages

PR here

because I already need it in two projects

What's your use case, if I can ask?

@phiresky
Copy link
Contributor Author

phiresky commented Nov 5, 2017

allow to detect a strict type programmatically, refinements are "opaque"

I don't really understand, can you elaborate?
I really just copied the definition of RefinementType<> for my implementation, and adjusted it because I wanted a better error message, while you used refinement directly.

PR here

I'll comment seperately on the PR.

What's your use case, if I can ask?

I'm using socket.io to send method calls (with or without response) from between client and server. I wrote complete type descriptions, so the arguments of every call of this is fully typed at compile-time. But the client data is obviously untrusted, so I check client message with io-ts before passing them to the server-side handlers.

I don't want to have clients be able to send extra data, for two reasons:

  1. Potential of DOS attack: I pass on this data to different servers via Redis pubsub, so larger amounts of data than expected can cause delays. I already limit array lengths for example.
  2. Potential other attacks. For example, I might have a method getTransactionInformation: {userid, transactionid} → {...}. Which calls the internal method getTransactionInformation: {userid, transactionid, dontCheckUserId} → {...}. Admins have the full method descriptor, where they can optionally set dontCheckUserId to true to get information of transactions of other users. If I pass the call data directly to this function, non-admins could also set this parameter to true, even if the argument type is checked.

@gcanti
Copy link
Owner

gcanti commented Nov 5, 2017

Nice use cases, thanks for explaining.

I don't really understand, can you elaborate?

Refinements are not easily introspectable, with StrictType you could define a function like this

type InterfaceLike<P extends t.Props> = t.InterfaceType<P> | t.StrictType<t.InterfaceType<P>>

export function makestrict<P extends t.Props>(type: InterfaceLike<P>): t.StrictType<t.InterfaceType<P>> {
  switch (type._tag) {
    case 'InterfaceType':
      return t.strict(type)
    case 'StrictType':
      return type
  }
}

or like this

export type HTML = string

export function makeForm(type: t.StringType | t.InterfaceType<any> | t.StrictType<any>): HTML {
  switch (type._tag) {
    case 'StringType':
      return 'a textbox...'
    case 'InterfaceType':
      return 'a form...'
    case 'StrictType':
      return makeForm(type.type)
  }
}

@gcanti gcanti closed this as completed in b9ea1f3 Nov 5, 2017
@gcanti
Copy link
Owner

gcanti commented Nov 7, 2017

@phiresky btw, speaking of validation and security, you may want to take a look at this post. It shows a technique which is great for validating user input to a web application. You can ensure with (almost) zero overhead that the data is validated once and only once everywhere that it needs to be, or else you get a compile-time error.

It was written for Flow but it can adapted to TypeScript, something like

// dummy implementation
const isAlpha = (s: string): boolean => false

export type State = 'Unvalidated' | 'Validated'

export class Data<M extends State> {
  readonly M: M
  private constructor(readonly input: string) {}

  /**
   * since we don’t export the constructor itself,
   * users with a string can only create Unvalidated values
   */
  static make(input: string): Data<'Unvalidated'> {
    return new Data(input)
  }

  /**
   * returns none if the data doesn't validate
   */
  static validate(
    data: Data<'Unvalidated'>
  ): Option<Data<'Validated'>> {
    return isAlpha(data.input) ? some(data as any) : none
  }

  static toUpperCase<M extends State>(data: Data<M>): Data<M> {
    return new Data(data.input.toUpperCase())
  }
}

/**
 * can only be fed the result of a call to validate!
 */
export function use(data: Data<'Validated'>): void {
  console.log(`using ${data.input}`)
}

@phiresky
Copy link
Contributor Author

phiresky commented Nov 7, 2017

@gcanti Interesting. I wanted to do something similar at some point, but then I decided it's better to validating everything that passes through generically before it reaches the actual server code. I've not yet ever wanted any of the unvalidated data, meaning for now I can just generally reject anything that fails the typecheck.

This way I can just handle everything as if it is trusted data and with normal Promises / async.

class Handler extends ClientSocketHandler<MySchema> {
    async someMessage(args) {
        // do stuff
        // args and callbackvalue are typechecked for the specific message
        return callbackvalue
    }
}

instead of the corresponding

socket.on("someMessage", (...args, callback) => {
   const validation = t.validate(args, MyArgType);
   if(isLeft(validation)) callback(someerror);
   else {
      // do stuff
      callback(null, callbackvalue));
   }
});

Here is my code: https://gist.github.com/phiresky/bd9c4c1d089e2248d038e578f737d555
Small example included.

I do 100% correct compile-time and server-side runtime type checking using some beautiful nested mapped types.

It's obviously specialized for socket.io; though it should be possible to make it more general to work with any kind of remote calls and events. I did the same thing for HTTP calls, but it wasn't general enough to integrate into the same code.

This is also why I opened #53 btw, because many types don't survive serialization and deserialization cleanly, which is pretty annoying.
(e.g. JSON.parse(JSON.stringify(new Date())) instanceof Date === false)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants