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

Type inference fails #41712

Closed
newcat opened this issue Nov 28, 2020 · 4 comments · Fixed by #48538
Closed

Type inference fails #41712

newcat opened this issue Nov 28, 2020 · 4 comments · Fixed by #48538
Labels
Fix Available A PR has been opened for this issue Needs Investigation This issue needs a team member to investigate its status.
Milestone

Comments

@newcat
Copy link

newcat commented Nov 28, 2020

TypeScript Version: 4.2.0-dev.20201127

Search Terms:

  • typescript type inference
  • typescript type not inferred correctly
  • typescript mapped types

Code

interface INodeIO<T = any> {
    value: T;
}

type IODefinition = Record<string, INodeIO>;
type IODefinitionValues<D extends IODefinition> = {
    [K in keyof D]: D[K] extends INodeIO<infer T> ? T : never;
};
type CalcFunc<I extends IODefinition, O extends IODefinition> = (
    inputs: IODefinitionValues<I>
) => IODefinitionValues<O>;

type NodeDefinition<I extends IODefinition, O extends IODefinition> = {
    setup(): { inputs: I; outputs: O };
    calculate?: CalcFunc<I, O>;
};

function defineNode<I extends IODefinition, O extends IODefinition>(def: NodeDefinition<I, O>) {
    return def; // placeholder
}

// T1 is correctly identified as
// T1 = { hello: number; }
type T1 = IODefinitionValues<{ hello: { value: number } }>;

// T2 is identified as (inputs: IODefinitionValues<{ test: { value: number; };}>) => IODefinitionValues<{ ret: { value: boolean; }; }>
type T2 = CalcFunc<{ test: { value: number } }, { ret: { value: boolean } }>;


// This works as expected, Intellisense is working and I get type errors when using the wrong type
const calcFuncTest: T2 = (def) => ({ ret: Boolean(def.test) });
calcFuncTest({ test: 3 });

defineNode({
    setup() {
        return {
            inputs: {
                a: { value: 3 },
                b: { value: "bar" }
            },
            outputs: {
                c: { value: false },
                d: { value: "foo" }
            }
        };
    },
    calculate(inputs) {
        return {
            c: inputs.v, // (1)
            d: inputs.a, // (2)
        };
    }
});

Expected behavior:

  • (1) should give an error, since inputs.v does not exist
  • (2) should give an error, since inputs.a is a number, while d is a string

Actual behavior:
The code above compiles without errors. Also, Intellisense is not working at the calculate function inside defineNode.

Playground Link:
Link

Related Issues:

@jcalz
Copy link
Contributor

jcalz commented Dec 1, 2020

Could you provide a more minimal example?

Possibly related to #33042, #39838, #38872, maybe others?

@RyanCavanaugh RyanCavanaugh added the Needs Investigation This issue needs a team member to investigate its status. label Dec 1, 2020
@RyanCavanaugh RyanCavanaugh added this to the Backlog milestone Dec 1, 2020
@newcat
Copy link
Author

newcat commented Dec 1, 2020

Tried shrinking it a bit, wasn't too successful (new playground link). But I replaced the domain-specific language with some generic terms so I hope it is a bit easier to understand. I'll explain it in more detail below.

class Wrapper<T = any> {
    public value?: T;
}

type WrappedMap = Record<string, Wrapper>;
type Unwrap<D extends WrappedMap> = {
    [K in keyof D]: D[K] extends Wrapper<infer T> ? T : never;
};

type MappingComponent<I extends WrappedMap, O extends WrappedMap> = {
    setup(): { inputs: I; outputs: O };
    map?: (inputs: Unwrap<I>) => Unwrap<O>;
};

declare function createMappingComponent<I extends WrappedMap, O extends WrappedMap>(def: MappingComponent<I, O>): void;

createMappingComponent({
    setup() {
        return {
            inputs: {
                num: new Wrapper<number>(),
                str: new Wrapper<string>()
            },
            outputs: {
                bool: new Wrapper<boolean>(),
                str: new Wrapper<string>()
            }
        };
    },
    map(inputs) {
        return {
            bool: inputs.nonexistent, // (1)
            str: inputs.num,          // (2)
        };
    }
});

Essentially, the code is about creating a MappingComponent, which has some defined inputs I and some defined outputs O. In the setup() method, these inputs and outputs are defined as a Record<string, Wrapper> each. These wrapper instances can be seen as placeholders.

The map function receives the values of the input placeholders (input Wrapper), so in this example it would be { num: <some number>, str: <some string> }. It needs to return values for the output placeholders like this { bool: <some boolean>, str: <some string> }.

Similar to above, I expect

  • (1) should give an error, since inputs.nonexistent does not exist
  • (2) should give an error, since inputs.num is a number, while str should be a string

Interestingly, the outputs O are inferred correctly. So for example, returning bool: 3 in the map function creates an error, since 3 is not a boolean.

@RyanCavanaugh
Copy link
Member

I don't see any evidence of a bug here. The inference for a setup like this is extremely complex, but if the type parameters are set up correctly then you would see the expected errors.

class Wrapper<T> {
    public value?: T;
}

type WrappedMap<Values> = Record<string, Wrapper<Values>>;
type Unwrap<D extends WrappedMap<unknown>> = {
    [K in keyof D]: D[K] extends Wrapper<infer T> ? T : never;
};

type MappingComponent<I extends WrappedMap<unknown>, O extends WrappedMap<unknown>> = {
    setup(): { inputs: I; outputs: O };
    map?: (inputs: Unwrap<I>) => Unwrap<O>;
};

declare function createMappingComponent<I extends WrappedMap<unknown>, O extends WrappedMap<unknown>>(def: MappingComponent<I, O>): MappingComponent<I, O>;

const res = createMappingComponent({
    setup() {
        return {
            inputs: {
                num: new Wrapper<number>(),
                str: new Wrapper<string>()
            },
            outputs: {
                bool: new Wrapper<boolean>(),
                str: new Wrapper<string>()
            }
        };
    },
    // Errors as expected
    map(inputs) {
        return {
            bool: inputs.nonexistent, // (1)
            str: inputs.num,          // (2)
        };
    }
});

Because there's a Record<string, in the definition of WrappedMap, you're not going to get an error on inputs.nonexistent, but it will produce unknown which triggers a downstream error.

@newcat
Copy link
Author

newcat commented Dec 2, 2020

I tried your code, however, I get errors on correct code now, because the values of inputs are of type unknown now. Example of a correct map function in the context above:

map(inputs) {
    return {
        bool: true,
        str: inputs.str,
    };
}

Results in this error:

Type '(inputs: Unwrap<Record<string, Wrapper<unknown>>>) => { bool: boolean; str: unknown; }' is not assignable to type '(inputs: Unwrap<Record<string, Wrapper<unknown>>>) => Unwrap<{ bool: Wrapper<boolean>; str: Wrapper<string>; }>'.
  Call signature return types '{ bool: boolean; str: unknown; }' and 'Unwrap<{ bool: Wrapper<boolean>; str: Wrapper<string>; }>' are incompatible.
    The types of 'str' are incompatible between these types.
      Type 'unknown' is not assignable to type 'string'.(2322)
input.tsx(12, 5): The expected type comes from property 'map' which is declared here on type 'MappingComponent<Record<string, Wrapper<unknown>>, { bool: Wrapper<boolean>; str: Wrapper<string>; }>'

This may very well be a design limitation, because when the map function is not present, the type of res is correctly inferred as

const res: MappingComponent<{
    num: Wrapper<number>;
    str: Wrapper<string>;
}, {
    bool: Wrapper<boolean>;
    str: Wrapper<string>;
}>

As soon as I write the mapping function, the type of res changes to:

const res: MappingComponent<Record<string, Wrapper<unknown>>, Record<string, Wrapper<unknown>>>

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Fix Available A PR has been opened for this issue Needs Investigation This issue needs a team member to investigate its status.
Projects
None yet
4 participants