-
Notifications
You must be signed in to change notification settings - Fork 32
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
TypeScript typings #105
Comments
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:
What's missing:
The
|
Thanks for the great work @benneq! |
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:
because this does not work as expected:
|
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 After that, I should hopefully be able to finish the Some further explanations:
|
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. |
I'll try that and will report back in a few minutes :) EDIT: So it can be for example:
Is that correct? EDIT2: I still don't get it 😞
This is now allowed by TypeScript, but spected won't give any results for Could you provide some simple examples without using ramda? 🤣 |
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:
But what about this:
Should this do anything meaningful? And when to use this:
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 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 [
{
"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:
The signature is And for objects it's: The last option would be something like:
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. |
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: #106 (comment) ) |
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:
too? |
const data = {
foo: ["a", "b", "c"]
} This is like the users example, sure. |
Let's clarify:
|
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 :(
And the other problem is, that you are allowed to write |
It's getting better and better! I've renamed all the types and added some more:
I've removed the The type inference for tuples seems now working correctly! No more need for At the moment it is still allowed to write 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 EDIT: Pushed the current typings. Still waiting for merge: DefinitelyTyped/DefinitelyTyped#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 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.
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 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 |
Thanks to jack-williams we now have working booleans! microsoft/TypeScript#29477 I didn't know about this "distributive conditional types" in TypeScript. Really weird stuff 😆 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 [object]
? (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 [object]
? 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>}; |
I created a pull request in the DefinitelyTyped repository: DefinitelyTyped/DefinitelyTyped#31953
It's basically working, but some things could be nicer (or better typed):
Spec
andResult
SpecValue
is a functionEspecially point 2 is hard for me to figure out...
Example(s):
I have no clue what those function signatures for
a
,b
andc
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 😆
The text was updated successfully, but these errors were encountered: