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

boolean in recursive generic inferred as any (string and number work correctly) #29477

Closed
benneq opened this issue Jan 18, 2019 · 4 comments
Closed

Comments

@benneq
Copy link

benneq commented Jan 18, 2019

TypeScript Version: 3.2.4 (also happened with 3.2.2, no information about prior versions)

Search Terms:
generic(s), boolean, infer(red), any

Code
I've removed as much of the type definitions as possible while still getting the error.
In the test code you can see, that the error happens somewhere in the recursive part.
I've included the same 2 cases for string and number, too (where they work). the only issue comes with type boolean.

declare function spected<INPUT, SPEC extends SpecValue<INPUT> = SpecValue<INPUT>>(spec: SPEC, input: INPUT): any;

export type Predicate<INPUT> = (value: INPUT) => boolean

export type SpecValue<INPUT> =
    INPUT extends {[key: string]: any}
        ? {[key in keyof INPUT]: SpecValue<INPUT[key]>}
        : Predicate<INPUT> | ((value: INPUT) => Predicate<INPUT>);
        
// TEST CODE #1:
const data = {
    str1: "",
    str2: "",
    number1: 0,
    number2: 0,
    boolean1: true,
    boolean2: true
}

const res = spected<typeof data>(
    {
        str1: (value) => false,
        str2: (value) => (value) => false,
        number1: (value) => false,
        number2: (value) => (value) => false,
        boolean1: (value) => true, // ERROR: value is of type any
        boolean2: (value) => (value) => true // ERROR: value is of type any (both)
    },
    data
);

// TEST CODE #2:
spected((value) => false, "");
spected((value) => (value) => false, "");
spected((value) => false, 0);
spected((value) => (value) => false, 0);
spected((value) => false, true); // works as expected
spected((value) => (value) => false, true); // works as expected

Expected behavior:
boolean should behave exactly the same as string and number

Actual behavior:
boolean behaves special

Playground Link:
http://www.typescriptlang.org/play/#src=declare%20function%20spected%3CINPUT%2C%20SPEC%20extends%20SpecValue%3CINPUT%3E%20%3D%20SpecValue%3CINPUT%3E%3E(spec%3A%20SPEC%2C%20input%3A%20INPUT)%3A%20any%3B%0D%0A%0D%0Aexport%20type%20Predicate%3CINPUT%3E%20%3D%20(value%3A%20INPUT)%20%3D%3E%20boolean%0D%0A%0D%0Aexport%20type%20SpecValue%3CINPUT%3E%20%3D%0D%0A%20%20%20%20INPUT%20extends%20%7B%5Bkey%3A%20string%5D%3A%20any%7D%0D%0A%20%20%20%20%20%20%20%20%3F%20%7B%5Bkey%20in%20keyof%20INPUT%5D%3A%20SpecValue%3CINPUT%5Bkey%5D%3E%7D%0D%0A%20%20%20%20%20%20%20%20%3A%20Predicate%3CINPUT%3E%20%7C%20((value%3A%20INPUT)%20%3D%3E%20Predicate%3CINPUT%3E)%3B%0D%0A%20%20%20%20%20%20%20%20%0D%0A%2F%2F%20TEST%20CODE%20%231%3A%0D%0Aconst%20data%20%3D%20%7B%0D%0A%20%20%20%20str1%3A%20%22%22%2C%0D%0A%20%20%20%20str2%3A%20%22%22%2C%0D%0A%20%20%20%20number1%3A%200%2C%0D%0A%20%20%20%20number2%3A%200%2C%0D%0A%20%20%20%20boolean1%3A%20true%2C%0D%0A%20%20%20%20boolean2%3A%20true%0D%0A%7D%0D%0A%0D%0Aconst%20res%20%3D%20spected%3Ctypeof%20data%3E(%0D%0A%20%20%20%20%7B%0D%0A%20%20%20%20%20%20%20%20str1%3A%20(value)%20%3D%3E%20false%2C%0D%0A%20%20%20%20%20%20%20%20str2%3A%20(value)%20%3D%3E%20(value)%20%3D%3E%20false%2C%0D%0A%20%20%20%20%20%20%20%20number1%3A%20(value)%20%3D%3E%20false%2C%0D%0A%20%20%20%20%20%20%20%20number2%3A%20(value)%20%3D%3E%20(value)%20%3D%3E%20false%2C%0D%0A%20%20%20%20%20%20%20%20boolean1%3A%20(value)%20%3D%3E%20true%2C%20%2F%2F%20ERROR%3A%20value%20is%20of%20type%20any%0D%0A%20%20%20%20%20%20%20%20boolean2%3A%20(value)%20%3D%3E%20(value)%20%3D%3E%20true%20%2F%2F%20ERROR%3A%20value%20is%20of%20type%20any%20(both)%0D%0A%20%20%20%20%7D%2C%0D%0A%20%20%20%20data%0D%0A)%3B%0D%0A%0D%0A%2F%2F%20TEST%20CODE%20%232%3A%0D%0Aspected((value)%20%3D%3E%20false%2C%20%22%22)%3B%0D%0Aspected((value)%20%3D%3E%20(value)%20%3D%3E%20false%2C%20%22%22)%3B%0D%0Aspected((value)%20%3D%3E%20false%2C%200)%3B%0D%0Aspected((value)%20%3D%3E%20(value)%20%3D%3E%20false%2C%200)%3B%0D%0Aspected((value)%20%3D%3E%20false%2C%20true)%3B%0D%0Aspected((value)%20%3D%3E%20(value)%20%3D%3E%20false%2C%20true)%3B

Related Issues:
none yet?

@jack-williams
Copy link
Collaborator

The problem stems from the fact that boolean is actually a union type true | false, and your conditional type SpecValue distributes over union. Compare these types to see where the problem comes from:

// type N = Predicate<number> | ((value: number) => Predicate<number>)
type N = SpecValue<(typeof data)["number1"]>;
// type B = Predicate<false> | ((value: false) => Predicate<false>) | Predicate<true> | ((value: true) => Predicate<true>)
type B = SpecValue<(typeof data)["boolean1"]>;

This definition of SpecValue gives the behaviour you want, I think.

export type SpecValue<INPUT> =
    [INPUT] extends [object]
        ? {[key in keyof INPUT]: SpecValue<INPUT[key]>}
        : Predicate<INPUT> | ((value: INPUT) => Predicate<INPUT>);

// type B = Predicate<boolean> | ((value: boolean) => Predicate<boolean>)
type B = SpecValue<(typeof data)["boolean1"]>;

Tag for searching purpose: distributive conditional type trouble

@benneq
Copy link
Author

benneq commented Jan 18, 2019

Yes, it looks like it works for that small example. But I still have trouble with my real code...
There I have to distinguish between "key-value objects", "arrays", and "primitives".

I'm not really sure what [INPUT] extends [object] does. Looks like "tuple with one element 'INPUT' extends tuple with one element of type object". But the object type also includes arrays.

Why can't I use it the other way around?

export type SpecValue<INPUT> =
    INPUT extends (string | number | true | false | boolean)
        ? Predicate<INPUT> | ((value: INPUT) => Predicate<INPUT>)
        : {[key in keyof INPUT]: SpecValue<INPUT[key]>}

Here's the real / full code, if you maybe want to have a look at:
(you can ignore ROOTINPUT, it just passes the initial input value down the tree, and has no other purpose)
http://www.typescriptlang.org/play/#src=declare%20function%20spected%3CROOTINPUT%2C%20SPEC%20extends%20SpecValue%3CROOTINPUT%2C%20ROOTINPUT%3E%20%3D%20SpecValue%3CROOTINPUT%2C%20ROOTINPUT%3E%3E(spec%3A%20SPEC%2C%20input%3A%20ROOTINPUT)%3A%20any%3B%0D%0A%0D%0Atype%20Predicate%3CINPUT%2C%20ROOTINPUT%3E%20%3D%20(value%3A%20INPUT%2C%20inputs%3A%20ROOTINPUT)%20%3D%3E%20boolean%3B%0D%0A%0D%0Atype%20ErrorMsg%3CINPUT%3E%20%3D%0D%0A%20%20%20%20%7C%20(string%20%7C%20number%20%7C%20boolean%20%7C%20symbol%20%7C%20null%20%7C%20undefined%20%7C%20object)%0D%0A%20%20%20%20%7C%20((value%3A%20INPUT%2C%20field%3A%20string)%20%3D%3E%20any)%3B%0D%0A%0D%0Aexport%20type%20Spec%3CINPUT%2C%20ROOTINPUT%20%3D%20any%3E%20%3D%20%5BPredicate%3CINPUT%2C%20ROOTINPUT%3E%2C%20ErrorMsg%3CINPUT%3E%5D%3B%0D%0A%0D%0Aexport%20type%20SpecArray%3CINPUT%2C%20ROOTINPUT%20%3D%20any%3E%20%3D%20Array%3CSpec%3CINPUT%2C%20ROOTINPUT%3E%3E%0D%0A%0D%0Aexport%20type%20SpecFunction%3CINPUT%2C%20ROOTINPUT%20%3D%20any%3E%20%3D%20INPUT%20extends%20ReadonlyArray%3Cinfer%20U%3E%0D%0A%20%20%20%20%3F%20(value%3A%20INPUT)%20%3D%3E%20ReadonlyArray%3CSpecArray%3CU%2C%20ROOTINPUT%3E%3E%0D%0A%20%20%20%20%3A%20INPUT%20extends%20%7B%5Bkey%3A%20string%5D%3A%20any%7D%0D%0A%20%20%20%20%20%20%20%20%3F%20(value%3A%20INPUT)%20%3D%3E%20SpecObject%3CINPUT%2C%20ROOTINPUT%3E%0D%0A%20%20%20%20%20%20%20%20%3A%20(value%3A%20INPUT)%20%3D%3E%20SpecArray%3CINPUT%2C%20ROOTINPUT%3E%3B%0D%0A%0D%0Aexport%20type%20SpecObject%3CINPUT%2C%20ROOTINPUT%20%3D%20any%3E%20%3D%20Partial%3C%7B%5Bkey%20in%20keyof%20INPUT%5D%3A%20SpecValue%3CINPUT%5Bkey%5D%2C%20ROOTINPUT%3E%7D%3E%0D%0A%0D%0Aexport%20type%20SpecValue%3CINPUT%2C%20ROOTINPUT%20%3D%20any%3E%20%3D%20INPUT%20extends%20ReadonlyArray%3Cany%3E%0D%0A%20%20%20%20%3F%20SpecArray%3CINPUT%2C%20ROOTINPUT%3E%20%7C%20SpecFunction%3CINPUT%2C%20ROOTINPUT%3E%0D%0A%20%20%20%20%20%20%20%20%3A%20INPUT%20extends%20%7B%5Bkey%3A%20string%5D%3A%20any%7D%0D%0A%20%20%20%20%20%20%20%20%20%20%20%20%3F%20SpecArray%3CINPUT%2C%20ROOTINPUT%3E%20%7C%20SpecFunction%3CINPUT%2C%20ROOTINPUT%3E%20%7C%20SpecObject%3CINPUT%2C%20ROOTINPUT%3E%0D%0A%20%20%20%20%20%20%20%20%20%20%20%20%3A%20SpecArray%3CINPUT%2C%20ROOTINPUT%3E%20%7C%20SpecFunction%3CINPUT%2C%20ROOTINPUT%3E%3B%0D%0A%20%20%20%20%20%20%20%20%20%20%20%20%0D%0A%0D%0A%0D%0Aconst%20data%20%3D%20%7B%0D%0A%20%20%20%20notValidatedString%3A%20''%2C%0D%0A%20%20%20%20notValidatedArray%3A%20%5B0%5D%2C%0D%0A%20%20%20%20notValidatedObject%3A%20%7B%7D%2C%0D%0A%20%20%20%20str1%3A%20%22%22%2C%0D%0A%20%20%20%20str2%3A%20%22%22%2C%0D%0A%20%20%20%20number1%3A%200%2C%0D%0A%20%20%20%20number2%3A%200%2C%0D%0A%20%20%20%20boolean1%3A%20true%2C%0D%0A%20%20%20%20boolean2%3A%20true%2C%0D%0A%20%20%20%20array1%3A%20%5B0%5D%2C%0D%0A%20%20%20%20array2%3A%20%5B0%5D%2C%0D%0A%20%20%20%20array3%3A%20%5B0%5D%2C%0D%0A%20%20%20%20emptyObj1%3A%20%7B%7D%2C%0D%0A%20%20%20%20emptyObj2%3A%20%7B%7D%2C%0D%0A%20%20%20%20emptyObj3%3A%20%7B%7D%2C%0D%0A%20%20%20%20obj1%3A%20%7B%20foo%3A%20%22bar%22%20%7D%2C%0D%0A%20%20%20%20obj2%3A%20%7B%20foo%3A%20%22bar%22%20%7D%2C%0D%0A%20%20%20%20obj3%3A%20%7B%20foo%3A%20%22bar%22%20%7D%2C%0D%0A%20%20%20%20obj4%3A%20%7B%20foo%3A%20%22bar%22%20%7D%2C%0D%0A%20%20%20%20obj5%3A%20%7B%20foo%3A%20%22bar%22%20%7D%0D%0A%7D%0D%0A%0D%0Atype%20B%20%3D%20SpecValue%3C(typeof%20data)%5B%22boolean1%22%5D%3E%3B%0D%0A%0D%0Aconst%20res%20%3D%20spected%3Ctypeof%20data%3E(%0D%0A%20%20%20%20%7B%0D%0A%20%20%20%20%20%20%20%20str1%3A%20%5B%5B(value)%20%3D%3E%20false%2C%20'err'%5D%5D%2C%0D%0A%20%20%20%20%20%20%20%20str2%3A%20(value)%20%3D%3E%20%5B%5B(value)%20%3D%3E%20false%2C%20'err'%5D%5D%2C%20%2F%2F%20doesn't%20work%20until%20%23104%20%23106%0D%0A%20%20%20%20%20%20%20%20number1%3A%20%5B%5B(value)%20%3D%3E%20false%2C%20'err'%5D%5D%2C%0D%0A%20%20%20%20%20%20%20%20number2%3A%20(value)%20%3D%3E%20%5B%5B(value)%20%3D%3E%20false%2C%20'err'%5D%5D%2C%20%2F%2F%20doesn't%20work%20until%20%23104%20%23106%0D%0A%20%20%20%20%20%20%20%20%2F%2F%20boolean1%3A%20%5B%5B(value)%20%3D%3E%20false%2C%20'err'%5D%5D%2C%20%2F%2F%20value%20is%20of%20type%20any...%0D%0A%20%20%20%20%20%20%20%20%2F%2F%20boolean2%3A%20(value)%20%3D%3E%20%5B%5B(value)%20%3D%3E%20false%2C%20'err'%5D%5D%2C%20%2F%2F%20value%20is%20of%20type%20any...%0D%0A%20%20%20%20%20%20%20%20array1%3A%20%5B%5B(value)%20%3D%3E%20false%2C%20'err'%5D%5D%2C%0D%0A%20%20%20%20%20%20%20%20array2%3A%20(value)%20%3D%3E%20%5B%5B%5B(value)%20%3D%3E%20false%2C%20'err'%5D%5D%5D%2C%0D%0A%20%20%20%20%20%20%20%20array3%3A%20(value)%20%3D%3E%20value.map(elem%20%3D%3E%20%5B%5B(value)%20%3D%3E%20false%2C%20'err'%5D%5D)%2C%0D%0A%20%20%20%20%20%20%20%20emptyObj1%3A%20%5B%5B(value)%20%3D%3E%20false%2C%20'err'%5D%5D%2C%0D%0A%20%20%20%20%20%20%20%20emptyObj2%3A%20(value)%20%3D%3E%20(%7B%7D)%2C%0D%0A%20%20%20%20%20%20%20%20emptyObj3%3A%20%7B%7D%2C%0D%0A%20%20%20%20%20%20%20%20obj1%3A%20%5B%5B(value)%20%3D%3E%20false%2C%20'err'%5D%5D%2C%0D%0A%20%20%20%20%20%20%20%20obj2%3A%20(value)%20%3D%3E%20(%7B%7D)%2C%0D%0A%20%20%20%20%20%20%20%20obj3%3A%20(value)%20%3D%3E%20(%7B%20foo%3A%20%5B%5B(value)%20%3D%3E%20false%2C%20'err'%5D%5D%20%7D)%2C%0D%0A%20%20%20%20%20%20%20%20obj4%3A%20%7B%7D%2C%0D%0A%20%20%20%20%20%20%20%20obj5%3A%20%7B%20foo%3A%20%5B%5B(value)%20%3D%3E%20false%2C%20'err'%5D%5D%20%7D%2C%0D%0A%20%20%20%20%7D%2C%0D%0A%20%20%20%20data%0D%0A)%3B%0D%0A%0D%0Aspected(%5B%5B(value)%20%3D%3E%20false%2C%20'err'%5D%5D%2C%20%22%22)%3B%0D%0Aspected((value)%20%3D%3E%20%5B%5B(value)%20%3D%3E%20false%2C%20'err'%5D%5D%2C%20%22%22)%3B%0D%0Aspected(%5B%5B(value)%20%3D%3E%20false%2C%20'err'%5D%5D%2C%200)%3B%0D%0Aspected((value)%20%3D%3E%20%5B%5B(value)%20%3D%3E%20false%2C%20'err'%5D%5D%2C%200)%3B%0D%0Aspected(%5B%5B(value)%20%3D%3E%20false%2C%20'err'%5D%5D%2C%20true)%3B%0D%0Aspected((value)%20%3D%3E%20%5B%5B(value)%20%3D%3E%20false%2C%20'err'%5D%5D%2C%20true)%3B%0D%0Aspected(%5B%5B(value)%20%3D%3E%20false%2C%20'err'%5D%5D%2C%20%5B0%5D)%3B%0D%0Aspected((value)%20%3D%3E%20%5B%5B%5B(value)%20%3D%3E%20false%2C%20'err'%5D%5D%5D%2C%20%5B0%5D)%3B%0D%0Aspected((value)%20%3D%3E%20value.map(elem%20%3D%3E%20%5B%5B(value)%20%3D%3E%20false%2C%20'err'%5D%5D)%2C%20%5B0%5D)%3B%0D%0Aspected(%5B%5B(value)%20%3D%3E%20false%2C%20'err'%5D%5D%2C%20%7B%7D)%3B%0D%0Aspected((value)%20%3D%3E%20(%7B%7D)%2C%20%7B%7D)%3B%0D%0Aspected(%7B%7D%2C%20%7B%7D)%3B%0D%0Aspected(%5B%5B(value)%20%3D%3E%20false%2C%20'err'%5D%5D%2C%20%7B%20foo%3A%20%22bar%22%20%7D)%3B%0D%0Aspected((value)%20%3D%3E%20(%7B%7D)%2C%20%7B%20foo%3A%20%22bar%22%20%7D)%3B%0D%0Aspected((value)%20%3D%3E%20(%7B%20foo%3A%20%5B%5B(value)%20%3D%3E%20false%2C%20'err'%5D%5D%20%7D)%2C%20%7B%20foo%3A%20%22bar%22%20%7D)%3B%0D%0Aspected(%7B%7D%2C%20%7B%20foo%3A%20%22bar%22%20%7D)%3B%0D%0Aspected%3C%7B%20foo%3A%20string%20%7D%3E(%7B%20foo%3A%20%5B%5B(value)%20%3D%3E%20false%2C%20'err'%5D%5D%20%7D%2C%20%7B%20foo%3A%20%22bar%22%20%7D)%3B

@jack-williams
Copy link
Collaborator

I'm not really sure what [INPUT] extends [object] does.

If you have a conditional type A extends B ? C : D, and A is a type parameter, the conditional type gets marked as distributive. Wrapping the check and extends type in [] is a way to turn that off.

[A] extends [B] ? C : D is a non distributive form of A extends B ? C : D.

I use object instead of { [key: string]: any } because it is shorter.

You can apply the same trick that; seems to work for me:

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>;

@benneq
Copy link
Author

benneq commented Jan 18, 2019

Yes, this works great! Thank you!

I didn't apply this trick to the Arrays. Guess I have to read some more about those distributive types.

But you can only use object instead of { [key: string]: any }, because there's the check for array before.

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