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 hole: A function with const generic does not infer the type if it is a spread parameter (tuple) #55033

Closed
Refzlund opened this issue Jul 15, 2023 · 8 comments
Labels
Working as Intended The behavior described is the intended behavior; this is not a bug

Comments

@Refzlund
Copy link

Refzlund commented Jul 15, 2023

Bug Report

🔎 Search Terms

is:issue is:open function generic tuple const
is:issue is:open in:title tuple const

Might solve; #54254 <T extends [a: string, b: number]>(...args: T)

🕗 Version & Regression Information

  • This is a overlooked type feature
  • This has been an immediate effect of including a new feature: const in function generics
  • This is the behaviour in every version I tried, and I reviewed the FAQ for entries about const

⏯ Playground Link

Playground link

The playground contains both a simplification and the actual use case.

The simplified example shows the core issue, that rest parameters will not be inferred using the function generic const.

The usage example is to demonstrate relevance in a factory pattern, and to ensure test coverage if necessary.

💻 Code

// * --- Simplified example --- * //

function expectedBehaviour<const K>(example: K) {
    return {} as K
}

const ok1 = expectedBehaviour({ test: 1 })
//    ^?



function unexpectedBehaviour<const K extends unknown[]>(...args: K) {
    return {} as K
}

const notOk1 = unexpectedBehaviour<[{ test: number }]>({ test: 1 })
//    ^?

// What it is:         const notOk1: [{ test: number }]
// What was expected:  const notOk1: [{ test: 1 }]





// * --- Usage example --- * //

function factory<const T extends unknown[]>(cb: (...args: T) => void) {
    return function call<const K extends T>(...args: K): K {
    //                     ^ Remove me
        return {} as K
    }
}

const t1 = factory((a: { test: number }, b: string) => {})({ test: 123 }, 'some string')
//    ^?

const t2 = factory((a: { test: number }, b: string) => {})({ test: 123 } as const, 'some string')
//    ^?

🙁 Actual behavior

What

A generic function does not apply const to contents of a rest parameter.

function fn<const K extends unknown[]>(...args: K): K

Why

It's a bug, as the tuple does not become literal.

🙂 Expected behavior

Function generic const makes a type literal, including rest parameters.




@Andarist
Copy link
Contributor

There is no bug in your simplified example. You should write this:

function unexpectedBehaviour<const K extends readonly unknown[]>(...args: readonly [...K]) {
    return {} as K
}

From the PR that introduced this feature:

When a const type parameter is constrained to an array type, that array type should include a readonly modifier; otherwise, inferences for the type parameter will fail to meet the constraint. [...] Without the readonly modifier in the constraint, inference defaults to unknown[] because an inferred readonly tuple type wouldn't be assignable to unknown[].

The other problem with this example is that you provide the explicit type argument to your call, one that is using a wider type. I'm not sure why do u expect other things here but if you provide explicit type arguments then inference doesn't even kick in so it's not a surprise that TS doesn't narrow the type based on the provided arguments.

However, the behavior with the showcased factory in the "Usage example" section definitely looks odd. It's odd that adding the const modifier you end up with a wider type than when not using t.

@Andarist
Copy link
Contributor

So the problem is that T in the factory gets inferred as a mutable array. So later on, with const, the inferred type (a narrow one) doesn't pass the constraint check because a readonly array isn't a subtype of the mutable array (it's the other way around) and so the inferred type falls back to the constraint (which is the mutable array inferred from the outer factory call).

This is a minimal repro case of the problem: TS playground.

@Refzlund
Copy link
Author

Refzlund commented Jul 16, 2023

Thank you @Andarist, that was quick!
I'm not sure about the relevance of what I encounter after following your correction is appropriate.

TS Playground

function factory<const T extends unknown[]>(cb: (...args: T) => void) {
    return function call<const K extends T>(...args: readonly [...K]) {
        return {} as K
    }
}

interface Options {
    test?: number,
    herp?: string
}


const caller = factory((options: Options) => {})

const t1 = caller({ derp: 'asd' })
//                  ^ '{ derp: string; }' is not assignable to parameter of type 'Options'

const t2 = caller({ test: 123, derp: 'asd' })
//             This is a assignable to Options?        

const t3: Options = { test: 123, derp: 'asd' }

In this example it seems that K loses it's ability to extend T after providing one valid property for T.

I would have expected it to always conform to the shape of T. I am not smart enough to assess whether this is relevant to this conversation, or is intentional or not.

My IDE does not provide intellisense for the first property neither (running TS Nightly):
image

Providing an invalid first property does give an error:
image

If I put the cursor in front as shown here, it actually does show the correct (and expected) properties:
image

Should I file this as a separate issue on microsoft/TypeScript?

@Andarist
Copy link
Contributor

Extra keys are always allowed. What you see is the excess property check which is more of a lint rule error than a type system violation.

To truly check for assignability I recommend this:

type Test = Source extends Target ? 1 : 0 // assignable if 1, not assignable if 0

@fatcerberus
Copy link

fatcerberus commented Jul 17, 2023

Extra keys are always allowed. What you see is the excess property check which is more of a lint rule error than a type system violation.

I really wish the EPC error would use different wording than "X is not assignable to Y"; it's misleading and confuses people into thinking there's a bug when they later discover that X sometimes is assignable to Y.

@fatcerberus
Copy link

fatcerberus commented Jul 17, 2023

btw, the Options type above is additionally subject to weak type detection which is enforced comprehensively, unlike the excess property check.

@RyanCavanaugh RyanCavanaugh added the Working as Intended The behavior described is the intended behavior; this is not a bug label Jul 25, 2023
@RyanCavanaugh
Copy link
Member

Everything here is working as intended for various reasons discussed above

@typescript-bot
Copy link
Collaborator

This issue has been marked as "Working as Intended" and has seen no recent activity. It has been automatically closed for house-keeping purposes.

@typescript-bot typescript-bot closed this as not planned Won't fix, can't repro, duplicate, stale Jul 28, 2023
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

Successfully merging a pull request may close this issue.

5 participants