-
Notifications
You must be signed in to change notification settings - Fork 328
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
Comments
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 }>' ] |
I know, but this library also has t.Integer even though to TS it's the same as a floating point number.
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. |
Well actually no, your proposed solution would
PR here
What's your use case, if I can ask? |
I don't really understand, can you elaborate?
I'll comment seperately on the PR.
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:
|
Nice use cases, thanks for explaining.
Refinements are not easily introspectable, with 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)
}
} |
@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}`)
} |
@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 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. |
I think a type that specifies that only the given interface properties are allowed would be useful. Working Implementation:
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.
The text was updated successfully, but these errors were encountered: