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 parameter's function properties referencing other properties of the same parameter are broken #60848

Open
krulod opened this issue Dec 23, 2024 · 3 comments

Comments

@krulod
Copy link

krulod commented Dec 23, 2024

πŸ”Ž Search Terms

generics, type parameters, dependent properties

πŸ•— Version & Regression Information

None of the versions I have tried work as expected.

⏯ Playground Link

https://www.typescriptlang.org/play/?ts=5.1.6#code/HYQwtgpgzgDiDGEAEAnA9gV2AEwPpuGQG8AoJcpAekqQBsIAXAciiSgZBQaQHcBLBgAskIJADMs8BnwJIhIBgC4yFakgC0I+IhgNWo4Bkgo+8JAAMYJsJwCeABXQwIXW+ZXk1mhDr0XsEM44EMAMjmjOrua8gqbCUIKYtNhIAEbIfAGhpiC0cmgWVnw2KA5OLgxuHlQ03tqBfhHSBLn+gSFZDABiks3A4ZGV0Tyx8MJ8+uK9MsC8AsKicCjgjC5pIFAQKbKW1nYDFVUUSAHwtJzIEsBSMycQYnyEADzwBOxIaKkAVkgQAB4MDqsUjHY4oCAgbAEWi2JBFEplCKHRRIQxgdIoarHAJBToHVwoz5fADaTHh+3KriYAF0sRQcR0Qt1pgR8ZUAPwogAU5NKhO+pN5iMGthpAEokABeAB8SAAbmhMtUAL7SrlE-lfMWapAkapqKAFMScX5yly2ISPADmvDQKAA1vpWP9nFIttUAg9CFykCDQeQhWzbCiAIwhgA0dPIDOCoSDoYjNVRBRc6BQUBVSDFHvujwgPr9-sDlMqCcj-uj7VjYRLwaQIYAzOGkyBZqm7Zns9jc97fVG4XtSvH6xH+zG8bWy2Oq50etc+sOuRKZb7lc21MAUyg06w+GI5IIXBlWJu4ZwVoDMcdlVm9cc1KkMNwRiFeMhISlFufIJf8m1cUyc43KytbmM2AhpOCICOmkEDwCAGCbAAdChOZevmfYVgOxQUkiBIjuWFbjkyw5hoR-pqAAKrYzhIEwPKDnWaIYsusoKpkTBIBMybcBsUB8FaoCpPQf6VLR9FCiiWD2puPDAKx8qKtgTBIbqWFqEImBWsIPDINgmTAEw3DsAoyAjHEcg0WZyAunB3CPAeyBLBeLgAIQiDgMSwvpKS7DhpTROkryQBY0mycA5jstOAGhEBC6TkgDH+bYCmFlh2EIv2a6diQyp6qAkCwAgyDoFgeAMDwBRpWoe5vqggTnIgIhTPOtxWHhlRzEIzU-ok2DNpawBWqwVqMKar66XwKABCgSE5mcFwtcBsyenmLxvNwRK-ACQKYRWUFQsAMIZbhIoosxLjRYycYJUSgqMUGNJXdWACyjB9UlCKavdyWPdS2qKUq15qhqHzfADW13vSPYYWlxzFh1dZkc9E6I1OWHEaEb2adgn12BKcMVmofkItE3GbvZwBiKmWwiKw5hhWgcnRLJzaPsZ0i0Hk5NoNw5hhuYKICCwzXmBdKDuOlFBCll5FIDe2b6jQ7N1fBsw2IYuTHa2FOmYUjHDPMmC8cAsLtcJEBgBm940OZYy2hgySwVxYAwGgUD8Rb4h2kgABEm4oDYtC+0tfRsAJoAMBg4LOn8rrSENFji5L0PoQW-YI2dBEoyRCXIxjM5MtjH2SSOBNJqe7bpv28OMbLOVKx5usJzaJN2Ib3WGr11onHu1PgqEB6tiO9VQI7fgOcPVd02+XNoXm6dYZnyLZwXMU1mjq9EYXWPvWguOlyGABM5dqFP24dlLAZ11h2XXreeUkAV0BwE1pU4LgQjgsQjeyXVo3cAYAUIQ1kpoBHeEsTa+4QEDk+PQMAc0bZaHgJkJkWtYR8CYGaXuUBXhmnBCkeQxligwGOuCZwChrSNxgacc4yww5oH3OYVahBog2HtNASyzhcEmF0LBQQIBsG5DAO7bgrxtx2RhPNOhlwWQrRhutYA7wtr-EBDgYE-YDrQjNg9BK4sc43U3ndMkujEZPTXtdZkrUQKI05IlUuxjl5Un+lKNiSkVQg2+JqCG3xpGLSuMtO46FFHKO+NtNR2ANFYS0UdHRv09FGAxAYjeWdHGmJFOY7e684ozCDHYvGfIwYkhMfEsxLiVzsWwB49UXiik+J+FDSsac9pFnSSvfOWTLGkVHOpGglcL7piQIecEbMnxDLQPgnuEy1htwCmwRIPB6bJxnrVXSb5wR3AWiYROzCd5WOWkGaIGwep7yqRY6sOSbFZwKSlVxLT0oy1vnLBWjSWzAGbj3WZbhbQOlYLQPgHCuKzF2evYu+8jmsF0nPbszTCbX1KVnDp-pMYpPaT0zpFy5GLkPiGVKFctw7hrtLG+FY74UBeTChe9za4IrRXLbs69un0uhtkrFCUbmhhPncogbzfgDKJfCzKTycrKiAA

πŸ’» Code

namespace round_one {
    // let's start with declaring a function that:
    // - accepts a numeric `primaryProperty`
    // - accepts `dependentProperty` which should be identical to `primaryProperty`
    // - accepts optional `dependentFunctionProperty` which is a function with a parameter based on `primaryProperty`
    declare function define<const obj extends {
        readonly primaryProperty: number
        dependentProperty: obj['primaryProperty']
        dependentFunctionProperty?: (primary: obj['primaryProperty']) => void
    }>(obj: obj): obj 

    // so far everything works as expected
    define( {
        primaryProperty: 11,
        dependentProperty: 11, // no errors
    } )
    define( {
        primaryProperty: 11,
        dependentProperty: 13, // an error
    } )
    define( {
        primaryProperty: 11,
        dependentProperty: 11,
        dependentFunctionProperty: () => {}, // no errors if there is no parameter
    } )

    // but when we add a parameter to `dependentFunctionProperty`, it breaks because...
    define( {
        primaryProperty: 11,
        dependentProperty: 11,
        // Type '(primary: number) => void' is not assignable to type '(primary: unknown) => void'. 
        // though we didn't state which type we expect in the parameter! and why did `primary` become `unknown`?
        dependentFunctionProperty: (primary) => {
            primary
        },
    } )
}

namespace round_two {
    // if we replace a function property with a method, things get even weirder.
    declare function define<const obj extends {
        readonly primaryProperty: number
        dependentProperty: obj['primaryProperty']
        dependentMethod(primary: obj['primaryProperty']): void
    }>(obj: obj): obj 

    define( {
        primaryProperty: 11,
        dependentProperty: 11,
        dependentMethod(primary) {
            // `primary` is not inferred as `unknown` now, but still is not `11`: it's a `number`
            primary
        },
    } )

    // but we can manually annotate `primary` without any problems
    // which would be impossible for "normal" function signatures expecting `number`
    define( {
        primaryProperty: 11,
        dependentProperty: 11,
        dependentMethod(primary: 11) { // no errors
            primary
        },
    } )

    // annotating `primary` with something different than 11 results in an error as well
    define( {
        primaryProperty: 11,
        dependentProperty: 11,
        dependentMethod(primary: 12) { // an error
            primary
        },
    } )
}

namespace round_three {
    // now we get to the weirdest part of the problem.
    // accidentally i've discovered that simply repeating
    // the declaration of `define` makes typescript behave almost correctly
    declare function define<const obj extends {
        readonly primaryProperty: number
        dependentProperty: obj['primaryProperty']
        dependentFunctionProperty?: (primary: obj['primaryProperty']) => void
    }>(obj: obj): obj
    declare function define<const obj extends {
        readonly primaryProperty: number
        dependentProperty: obj['primaryProperty']
        dependentFunctionProperty?: (primary: obj['primaryProperty']) => void
    }>(obj: obj): obj 

    define( {
        primaryProperty: 11,
        dependentProperty: 11,
        // no errors here, but hovering over `primary` shows `number` as if we were declaring `dependentFunctionProperty` as a method
        dependentFunctionProperty: (primary) => {
            primary
        },
    } )

    // annotating `primary` works like in `dependentMethod` as well
    define( {
        primaryProperty: 11,
        dependentProperty: 11,
        dependentFunctionProperty: (primary: 11) => { // no errors
            primary
        },
    } )
    define( {
        primaryProperty: 11,
        dependentProperty: 11,
        dependentFunctionProperty: (primary: 12) => { // an error
            primary
        },
    } )
}

πŸ™ Actual behavior

I'm not sure where to begin. Properties of a type parameter that reference other properties of the same type parameter seem to be completely untested in TypeScript.

In the example above, I have referenced a generic's property from other properties in three ways:

  • from a function property
  • from a method
  • from a function property, but declaration of the function is duplicated!

All of them are buggy, but in different ways.

πŸ™‚ Expected behavior

Speaking honestly, I was expecting from such dependent properties to not work at all, failing with some kind of circular constraint error, but now I believe they could become usable some day.

Additional information about the issue

No response

@Andarist
Copy link
Contributor

TS inference algorithm can't always unify everything in ways you'd like it to when some information is missing. It has to deduce it after all and sometimes it might fail. That's why you see different results when you annotate that parameter vs when you don't. With annotated parameters, TS is able to infer some things earlier - in a single inference pass.

I'm surprised by the different behavior of your arrow function vs method examples. Both are context-sensitive and I'd expect them to behave roughly the same way in this context.

That said, your first example can be fixed if you use a reverse-mapped type (TS playground):

namespace round_four {
  declare function define<
    const obj extends {
      readonly primaryProperty: number;
      dependentProperty: obj["primaryProperty"];
      dependentFunctionProperty?: (primary: obj["primaryProperty"]) => void;
    },
  >(obj: { [K in keyof obj]: obj[K] }): obj;

  define({
    primaryProperty: 11,
    dependentProperty: 11, // no errors
  });
  define({
    primaryProperty: 11,
    dependentProperty: 13, // an error
  });
  define({
    primaryProperty: 11,
    dependentProperty: 11,
    dependentFunctionProperty: () => {}, // no errors if there is no parameter
  });

  define({
    primaryProperty: 11,
    dependentProperty: 11,
    dependentFunctionProperty: (primary) => {
      primary; // nicely inferred as 11, even when `primary` parameter isn't annotated
    },
  });
}

@Andarist
Copy link
Contributor

Ah, the method variant typechecks in ur case only because its typed using a method syntax and methods are bivariant

@krulod
Copy link
Author

krulod commented Dec 24, 2024

Thank you! Should this issue be closed as not planned now? I still wonder why redeclaration has effect on the types, though at this point that is not a practical interest

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