spected
spected copied to clipboard
TypeScript typings
I created a pull request in the DefinitelyTyped repository: https://github.com/DefinitelyTyped/DefinitelyTyped/pull/31953
It's basically working, but some things could be nicer (or better typed):
- Generics for
Spec
andResult
- Typings if
SpecValue
is a function
Especially point 2 is hard for me to figure out...
Example(s):
const spec = {
a: [[ values => R.all((val) => false, values), 'msg']],
b: R.map(R.always([[(val) => false, 'msg']])),
c: R.map(() => [[(val) => false, 'msg']])
}
I have no clue what those function signatures for a
, b
and c
are. I know from your source code, that those functions have a single argument (= value
). But I have no clue about the return types.
I'm no Ramda / Functional Programming export. Though some help would be appreciated 😄 Especially when it comes to currying I'm lost 😆
Excellent thank you very much! I will try to have look in the coming days.
It finally got merged :)
I'd really need some help for the typings...
I'm one step further now:
declare function spected<ROOTINPUT, SPEC = Spec<ROOTINPUT>>(spec: SPEC, input: ROOTINPUT): Result<ROOTINPUT, SPEC>;
export type Predicate<INPUT, ROOTINPUT> = (value: INPUT, inputs: ROOTINPUT) => boolean;
export type ErrorMsg<INPUT> =
| (string | number | boolean | symbol | null | undefined | object) // = anything that's not a function
| ((value: INPUT, field: string) => any);
export type Spec<INPUT, ROOTINPUT = any> = Partial<{[key in keyof INPUT]: SpecValue<INPUT[key], ROOTINPUT>}>;
export type SpecValue<INPUT, ROOTINPUT = any> =
| ReadonlyArray<[Predicate<INPUT, ROOTINPUT>, ErrorMsg<INPUT>]>
| ((value: INPUT) => any) // HERE I NEED HELP
| Spec<INPUT, ROOTINPUT>;
// This is much much much better, but still not finished:
// export type Result<INPUT, SPEC> = INPUT extends {[key: string]: infer U}
// ? {[key in keyof INPUT]: Result<INPUT[key], any>}
// : true | string[];
export type Result<INPUT, SPEC> = {[key in keyof INPUT]: true | any[] | Result<INPUT[key], any>}
export default spected;
Changes:
- ErrorMsg had to be
string
or afunction
returning astring
. Now it can be anything. (Becausespected
allows anything to be within the errors array. Which is good!) - Generics! These are the main generics:
-
ROOTINPUT
which needs to be passed to the validation predicates:[[(val, rootInput) => val !== undefined, 'errmsg']]
-
INPUT
is the "current" input value, depending on where in the object tree you currently are -
SPEC
which is thevalidationRules
object (of TypeSpec
).
-
What's missing:
- Typings for
SpecValue
functions. (see comment in the code above) - A better
Result
type
The Result
type will become kinda complicated, I guess. The type of each result depends on the type of spec, as far as I know so far. HERE I NEED YOUR HELP!
- If spec is an array: The result will be an array. Right or wrong?
- If spec is an object: The result will be an object. Right or wrong?
- If spec is a function: The result will be ... ???
Thanks for the great work @benneq! Will finally have look tomorrow.
I will checkout your type definitions and add input to the missing parts.
The DefinitelyTyped repo doesn't include the latest version yet. So you may copy and paste the code from my comment above and use it.
There's a small problem with TypeScript's type inference for tuples. Though you should write your TypeScript code like this:
import spected, { Spec } from 'spected';
const data = {
foo: {
bar: 42
}
}
const rules: Spec<typeof data, typeof data> = {
foo: {
bar: [[(value) => value > 9000, "error"]]
}
}
const res = spected(rules, data);
because this does not work as expected:
spected({
foo: {
bar: [[(value) => value > 9000, "error"]] // it will recognize this as Array<Function | string>
} // instead of [Function, string]
}, data);
Thanks for the info! I will use the above type definitions. Let's see how far we can get with this.
First let's try to get the SpecValue
working. The only missing part should be the ((value: INPUT) => any)
(here it needs something better than any
)
After that, I should hopefully be able to finish the Result
type.
Some further explanations:
-
spected
is the spected function. It takes 2 args: 1. the spec (or rules), 2. the input (or data) -
Spec
is the validation spec for an object (it's getting reused for nested objects) -
SpecValue
are the different kinds of rules you can provide: 1.[[...]]
stuff (= array of[Predicate, ErrorMsg]
tuples), 2.() => ??
style, 3. nested object (=Spec
) -
Predicate
is used within the[[...]]
. The first part of the tuple is the predicate -
ErrorMsg
is also used within[[...]]
. The seconds part of the tuple. It can be anything, but if it's a function, it receives 2 args: 1. input value, 2. field name
Haven't tried it yet, but shouldn't this be the type?
((value: INPUT) => ReadonlyArray<[Predicate<INPUT, ROOTINPUT>, ErrorMsg<INPUT>]>
It should always return the predicates. Can you verify? I'm currently setting up TypeScript.
Correction:
((value: INPUT) => SpecValue<INPUT, ROOTINPUT = any>
It might return any of the three possible specValues. Not sure if we can do recursive type definitions with TypeScript, but this could work with an interface?
I'll try that and will report back in a few minutes :)
EDIT: So it can be for example:
const rules = {
foo: (values) => [[val => false, "error"]],
bar: (values) => (values) => (values) => [[val => false, "error"]],
baz: (values) => {
x: [[val => false, "error"]],
y: (yValues) => []
}
}
Is that correct?
EDIT2: I still don't get it 😞
const data = {
foo: 42
}
const rules = {
foo: (value) => [[val => false, "error"]]
}
This is now allowed by TypeScript, but spected won't give any results for foo
Could you provide some simple examples without using ramda? 🤣 Then I can figure out the correct typings
I will check.
Also check this part, I think you need to wrap the object in () in your example.
baz: (values) => ({
x: [[val => false, "error"]],
y: (yValues) => []
})
For objects: yes, true. I forgot that. This is working fine and gives the correct result:
const data = {
foo: {
bar: 42
}
}
const rules = {
foo: (value) => ({
bar: [[val => false, "error"]]
})
}
But what about this:
const data = {
foo: 42
}
const rules = {
foo: (value) => [[val => false, "error"]]
}
Should this do anything meaningful?
And when to use this:
const rules = {
foo: (value) => (value) => (value) => ???
}
Sorry for being such a functional-currying-ramda noob
@benneq Thank you very much for the very valuable feedback. You shouldn't have to understand ramda to use the library. This is excellent feedback and shows some potential on how to improve the developer experience.
Regarding the ramda example from the beginning of this:
Not sure if a
is valid, but b
and c
are the same, they expect an array of items and return an array of specifications for each item. Check this test https://github.com/25th-floor/spected/blob/master/test/index.js#L496
The input would be an array of users and the map function returns the rules for each user. I will rewrite the example for more clarification.
This is what calling b
or c
in your example would return:
[
{
"firstName": [
[
...,
"Minimum firstName length of 6 is required."
]
],
"lastName": [
[
...,
"capital letter missing"
]
]
},
{
"firstName": [
[
...,
"Minimum firstName length of 6 is required."
]
],
"lastName": [
[
...,
"capital letter missing"
]
]
},
{
"firstName": [
[
...,
"Minimum firstName length of 6 is required."
]
],
"lastName": [
[
...,
"capital letter missing"
]
]
}
]
So, you could also write the following f.e.:
{users:
[
{
"firstName": [
[
...,
"Minimum firstName length of 6 is required."
]
],
"lastName": [
[
...,
"capital letter missing"
]
]
},
{
"firstName": [
[
...,
"Minimum firstName length of 6 is required."
]
],
"lastName": [
[
null,
"capital letter missing"
]
]
},
{
"firstName": [
[
...,
"Minimum firstName length of 6 is required."
]
],
"lastName": [
[
...,
"capital letter missing"
]
]
}
]
}
or you could write it like this:
{
b: input => input.map(val => [[(val) => false, 'msg']]),
c: input => input.map(val => [[(val) => false, 'msg']]),
}
Does that help?
Ahh! That makes way more sense to me. Thank you!
So this is the same:
b: input => input.map(val => [[(val) => false, 'msg']])
c: Ramda.map([[(val) => false, 'msg']])
The signature is (value: INPUT) => ReadonlyArray<[Predicate<INPUT, ROOTINPUT>, ErrorMsg<INPUT>]>
And for objects it's:
(value: INPUT) => Spec<INPUT, ROOTINPUT>
The last option would be something like:
{
x: input => somethingElse => ???
}
Is this possible (or does it have any meaning) at all for spected?
I would neglect the last option, I don't think this could even work. Not sure what the result would be, but you need a very specific structure to make it work. The only valid use case for using a function in spected, is to validate against an unkown number of items, f.e. users, like in the example. If we can restrict any other possible options, this would be valuable actually.
Of course this can be restricted. TypeScript has an extremely mighty type system - in fact often overwhelming. You can have recursive structures, and you can have conditional types in generics, and lot's of other crazy stuff.
Maybe you should rethink this restriction (pointing to this: https://github.com/25th-floor/spected/issues/106#issuecomment-455181780 )
This is the only other use case where this approach makes sense:
const data = {
foo: {bar: 1}
};
const rules = {
foo: (value) => {
return {bar: [[val => false, "error"]]}
}
};
verify(rules, data); // => { foo: {bar: "error"}
And for:
const data = {
foo: ["a", "b", "c"]
}
too?
const data = {
foo: ["a", "b", "c"]
}
This is like the users example, sure.
Let's clarify:
if INPUT is Array then allow:
[[...]]
(val) => ...
if INPUT is Object then allow:
[[...]]
(val) => ...
{ ... }
else allow:
[[...]]
If the input is an object, we also have to return an object, if the input is an array, we have to return an array. The input/output have to match.
const data = {
foo: {bar: 1}
};
const rules = {
foo: (value) => {
return {bar: [[val => false, "error"]]}
}
};
const data2 = {
users: [{id:1, name: "foo"}]
}
const rules2 = {
users: input => input.map(val => [[(val) => false, 'msg']]),
}
Is the above definition helpful?
Thanks a lot! I guess that's it for the day... Now I have to see what TypeScript is capable of 😆 (or what I am capable of)
@benneq But no stress. It doesn't have to be explicit for now.
But I want to use it as soon as possible with TypeScript ;)
Sure, but you can define the type recursively for now, this will work.
((value: INPUT) => SpecValue<INPUT, ROOTINPUT>
Not tested, but this could be valid.
Yes this works. But again there are problems with type inference for tuples :(
const data = {
b: ["a", "b"]
}
const rules: Spec<typeof data, typeof data> = {
b: input => input.map(val => [[(val) => false, 'msg']])
// here [[(val) => false, 'msg']] is not detected as tuple [Predicate, ErrorMsg],
// but instead as Array<Predicate | ErrorMsg>
// so you've got to write this:
// input => input.map(val => [[(val) => false, 'msg']] as [Predicate<string, any>, ErrorMsg<string>][])
}
And the other problem is, that you are allowed to write (val) => (val) => (val) => ......
It's getting better and better!
I've renamed all the types and added some more:
-
SpecArray
is[[...]]
-
SpecFunction
is(value) => ...
- If
value
is an array, it must return aSpecArray
- Else it must return a
SpecObject
- If
-
SpecObject
is{ ... }
- Each key must be a
string
- Each value is a
SpecValue
- Each key must be a
-
SpecValue
is eitherSpecArray
,SpecFunction
orSpecObject
I've removed the exports
for Predicate
and ErrorMsg
.
The type inference for tuples seems now working correctly! No more need for as [...]
At the moment it is still allowed to write { x: (value) => ... }
when x
is neither array, nor object. But hopefully this this will work eventually when #104 and #106 are resolved 😃
Here are the newest typings:
declare function spected<ROOTINPUT, SPEC = SpecObject<ROOTINPUT, ROOTINPUT>>(spec: SPEC, input: ROOTINPUT): Result<ROOTINPUT, SPEC>;
type Predicate<INPUT, ROOTINPUT> = (value: INPUT, inputs: ROOTINPUT) => boolean;
type ErrorMsg<INPUT> =
| (string | number | boolean | symbol | null | undefined | object)
| ((value: INPUT, field: string) => any);
type SpecArrayElement<INPUT, ROOTINPUT> = [Predicate<INPUT, ROOTINPUT>, ErrorMsg<INPUT>];
export type SpecArray<INPUT, ROOTINPUT> = ReadonlyArray<SpecArrayElement<INPUT, ROOTINPUT>>;
export type SpecFunction<INPUT, ROOTINPUT> = INPUT extends ReadonlyArray<infer U>
? (value: INPUT) => ReadonlyArray<SpecArray<U, ROOTINPUT>>
: (value: INPUT) => SpecObject<INPUT, ROOTINPUT>;
export type SpecObject<INPUT, ROOTINPUT> = Partial<{[key in keyof INPUT]: SpecValue<INPUT[key], ROOTINPUT>}>;
export type SpecValue<INPUT, ROOTINPUT> =
| SpecArray<INPUT, ROOTINPUT>
| SpecFunction<INPUT, ROOTINPUT>
| SpecObject<INPUT, ROOTINPUT>;
export type Result<INPUT, SPEC> = {[key in keyof INPUT]: true | any[] | Result<INPUT[key], any>};
export default spected;
And some working example:
const res = spected(
{
// input's type is inferred as "string[]"
// val's type is inferred as "string"
b: (input) => [[[(val) => val === 'b', 'err']]],
// input's type is inferred as "{ d: number }"
// val's type is inferred as "number"
c: (input) => ({ d: [[(val) => val > 9000, 'err']] }),
// input's type is inferred as "number[]"
// val's type is inferred as "number"
d: input => input.map(val => [[(val) => val > 9000, 'msg']])
},
{
a: 42,
b: ["a", "b"],
c: {
d: 42
},
d: [9000, 9001]
}
);
When using ramda, it won't correctly infer the types 😞 But then you still can use this:
const data = {
b: ["a", "b"],
}
const rules: SpecObject<typeof data, typeof data> = {
b: R.map(() => [[(val) => val === 'b', 'err']]),
}
const res = spected(rules, data);
Next step: The Result
type
EDIT: Pushed the current typings. Still waiting for merge: https://github.com/DefinitelyTyped/DefinitelyTyped/pull/32173
I just played some more with the typings. It's driving me nuts! 🤣
First: VSCode sometimes takes really long to refresh the typings in my code after I changed the spected typings. Before I figured that out, I have rewritten everything a hundred times, because there was always something wrong. Sometimes really strange type inference appeared, where value: number
was suddenly value: ROOTINPUT extends {}
. And I was just WTF ?!
I guess the recursive stuff is taking some time to compute.
Now I always wait a few more seconds, and then change the code a bit, (un)comment some lines, and THEN check the typings...
Second: TypeScript has some really strange behavior.
- Somehow
boolean
s aren't recognized within the typings. (see code below) - Somehow
SpecArray
isn't allowed to beReadonlyArray
The Code:
Typings:
declare function spected<ROOTINPUT, SPEC extends SpecValue<ROOTINPUT, ROOTINPUT> = SpecValue<ROOTINPUT, ROOTINPUT>>(spec: SPEC, input: ROOTINPUT): Result<ROOTINPUT, SPEC>;
type Predicate<INPUT, ROOTINPUT> = (value: INPUT, inputs: ROOTINPUT) => boolean;
type ErrorMsg<INPUT> =
| (string | number | boolean | symbol | null | undefined | object)
| ((value: INPUT, field: string) => any);
export type Spec<INPUT, ROOTINPUT = any> = [Predicate<INPUT, ROOTINPUT>, ErrorMsg<INPUT>];
export type SpecArray<INPUT, ROOTINPUT = any> = Array<Spec<INPUT, ROOTINPUT>>
export type SpecFunction<INPUT, ROOTINPUT = any> = INPUT extends ReadonlyArray<infer U>
? (value: INPUT) => ReadonlyArray<SpecArray<U, ROOTINPUT>>
: INPUT extends {[key: string]: any}
? (value: INPUT) => SpecObject<INPUT, ROOTINPUT>
: (value: INPUT) => SpecArray<INPUT, ROOTINPUT>;
export type SpecObject<INPUT, ROOTINPUT = any> = Partial<{[key in keyof INPUT]: SpecValue<INPUT[key], ROOTINPUT>}>
export type SpecValue<INPUT, ROOTINPUT = any> = INPUT extends ReadonlyArray<any>
? SpecArray<INPUT, ROOTINPUT> | SpecFunction<INPUT, ROOTINPUT>
: INPUT extends {[key: string]: any}
? SpecArray<INPUT, ROOTINPUT> | SpecFunction<INPUT, ROOTINPUT> | SpecObject<INPUT, ROOTINPUT>
: SpecArray<INPUT, ROOTINPUT> | SpecFunction<INPUT, ROOTINPUT>;
export type Result<INPUT, SPEC> = {[key in keyof INPUT]: true | any[] | Result<INPUT[key], any>};
export default spected;
Tests:
const data = {
notValidatedString: '',
notValidatedArray: [0],
notValidatedObject: {},
str1: "",
str2: "",
number1: 0,
number2: 0,
boolean1: true,
boolean2: true,
array1: [0],
array2: [0],
array3: [0],
emptyObj1: {},
emptyObj2: {},
emptyObj3: {},
obj1: { foo: "bar" },
obj2: { foo: "bar" },
obj3: { foo: "bar" },
obj4: { foo: "bar" },
obj5: { foo: "bar" }
}
const res = spected<typeof data>(
{
str1: [[(value) => false, 'err']],
str2: (value) => [[(value) => false, 'err']], // doesn't work until #104 #106
number1: [[(value) => false, 'err']],
number2: (value) => [[(value) => false, 'err']], // doesn't work until #104 #106
// boolean1: [[(value) => false, 'err']], // value is of type any...
// boolean2: (value) => [[(value) => false, 'err']], // value is of type any...
array1: [[(value) => false, 'err']],
array2: (value) => [[[(value) => false, 'err']]],
array3: (value) => value.map(elem => [[(value) => false, 'err']]),
emptyObj1: [[(value) => false, 'err']],
emptyObj2: (value) => ({}),
emptyObj3: {},
obj1: [[(value) => false, 'err']],
obj2: (value) => ({}),
obj3: (value) => ({ foo: [[(value) => false, 'err']] }),
obj4: {},
obj5: { foo: [[(value) => false, 'err']] },
},
data
);
For the boolean
issue, you can still write it like this and it works:
const data = {
boolean1: true,
boolean2: true
}
const res = spected<typeof data>(
{
boolean1: [[(value) => true, 'err']] as SpecArray<boolean, typeof data>,
boolean2: ((value: boolean) => [[(value: boolean) => true, 'err']]) as SpecFunction<boolean, typeof data>,
},
data
);
Tests for the future (#104 #106) (already supported by the typings):
spected([[(value) => false, 'err']], "");
spected((value) => [[(value) => false, 'err']], "");
spected([[(value) => false, 'err']], 0);
spected((value) => [[(value) => false, 'err']], 0);
spected([[(value) => false, 'err']], true);
spected((value) => [[(value) => false, 'err']], true);
spected([[(value) => false, 'err']], [0]);
spected((value) => [[[(value) => false, 'err']]], [0]);
spected((value) => value.map(elem => [[(value) => false, 'err']]), [0]);
spected([[(value) => false, 'err']], {});
spected((value) => ({}), {});
spected({}, {});
spected([[(value) => false, 'err']], { foo: "bar" });
spected((value) => ({}), { foo: "bar" });
spected((value) => ({ foo: [[(value) => false, 'err']] }), { foo: "bar" });
spected({}, { foo: "bar" });
spected<{ foo: string }>({ foo: [[(value) => false, 'err']] }, { foo: "bar" }); // type inference not working
// must be explicitly provided