Skip to content

Values of string literal type are ignored in generic function #31732

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

Closed
maxkomarychev opened this issue Jun 3, 2019 · 4 comments
Closed

Values of string literal type are ignored in generic function #31732

maxkomarychev opened this issue Jun 3, 2019 · 4 comments
Assignees
Labels
Working as Intended The behavior described is the intended behavior; this is not a bug

Comments

@maxkomarychev
Copy link

maxkomarychev commented Jun 3, 2019

String literal types are ignored and treated as just "string" when working with generics.

Also the line ("Test 3") which is expectedly fails to compile the error message contains this:

        Type 'number' is not assignable to type '"three" | "four"'.

but actual problem in line "Test 2" seems to ignore this fact

TypeScript Version:

$ tsc --version
Version 3.5.1

Search Terms:

Code

interface PropTypes {
    one: number,
    two: "three"|"four"
}

type ComponentType<P> = (props:P) => any

const MyCompnent = (props: PropTypes) => console.log(props)
MyCompnent({ one: 100, two: "three" })

function decorate<P>(props: P): (Wrapped: ComponentType<P>) => ComponentType<P> {
    return function wrap<P>(Wrapped: ComponentType<P>): ComponentType<P> {
        return (props: P) => console.log({ ...props })
    }
}

// Test 1: should work, all is good
const enhanced1 = decorate({ one: 42, two: "three" })(MyCompnent)
// Test 2: should not work because "two" only allows "three" or "four" as values
const enhanced2 = decorate({ one: 42, two: "wut" })(MyCompnent)
// Test 3: does not work because "two" should be "string", not "number"
const enhanced3 = decorate({ one: 42, two: 111 })(MyCompnent)

Expected behavior:

the following piece of code:

// Test 2: should not work because "two" only allows "three" or "four" as values
const enhanced2 = decorate({ one: 42, two: "wut" })(MyCompnent)

should not be compiled since MyComponent does not allow value "wut" for field "two".

Actual behavior:

Bug.ts:22:51 - error TS2345: Argument of type '(props: PropTypes) => void' is not assignable to parameter of type 'ComponentType<{ one: number; two: number; }>'.
  Types of parameters 'props' and 'props' are incompatible.
    Type '{ one: number; two: number; }' is not assignable to type 'PropTypes'.
      Types of property 'two' are incompatible.
        Type 'number' is not assignable to type '"three" | "four"'.

22 const enhanced3 = decorate({ one: 42, two: 111 })(MyCompnent)

Playground Link:

playground

Related Issues:

@RyanCavanaugh RyanCavanaugh added In Discussion Not yet reached consensus Suggestion An idea for TypeScript labels Jun 13, 2019
@RyanCavanaugh
Copy link
Member

@ahejlsberg I think this would be covered by the reverse return type inference we were discussing yesterday?

@ahejlsberg
Copy link
Member

@RyanCavanaugh No, generic return type inference doesn't cover this. When the result of a function call is the source of an assignment and we have a known type on the target side we'll perform generic return type inference. However, in this example, the result of the first function call is the function value for the second function call, so there is no contextual typing.

@ahejlsberg
Copy link
Member

This is working as intended. In --strict mode all three calls are errors. Since there is no contextual type for the argument to decorate we infer type { one: number, two: string } in the first two calls and { one: number, two: number } in the last call. This is less specific than the PropTypes required by MyCompnent and thus an error. You can force inference of literal types using a const assertion:

const enhanced1 = decorate({ one: 42, two: "three" } as const)(MyCompnent)
const enhanced2 = decorate({ one: 42, two: "wut" } as const)(MyCompnent)
const enhanced3 = decorate({ one: 42, two: 111 } as const)(MyCompnent)

The first call then works and the second two fail as expected.

Now, the reason the first two calls aren't errors in non-strict mode is that we check parameter types bivariantly when relating function types (which is unsound). We default to non-strict for reasons of backwards compatibility, but we definitely recommend using --strict or at least --strictFunctionTypes.

@ahejlsberg ahejlsberg added Working as Intended The behavior described is the intended behavior; this is not a bug and removed In Discussion Not yet reached consensus Suggestion An idea for TypeScript labels Jun 19, 2019
@ahejlsberg
Copy link
Member

Also, #31243 is a suggestion to perform generic return type inference for functions returning functions that are immediately invoked. That would cause us to contextually type the argument to decorate by PropTypes which would produce the expected outcome without const assertions.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Working as Intended The behavior described is the intended behavior; this is not a bug
Projects
None yet
Development

No branches or pull requests

3 participants