Skip to content

Inconsistent behavior with type inference for generic contraints #58009

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
DanielBertocci opened this issue Mar 30, 2024 · 7 comments
Closed
Labels
Question An issue which isn't directly actionable in code

Comments

@DanielBertocci
Copy link

DanielBertocci commented Mar 30, 2024

🔎 Search Terms

type, type inference, generic constraints

🕗 Version & Regression Information

  • This is the behavior in every version I tried, and I reviewed the FAQ for entries. Tested on version 4.3.3 and 5.5.0-dev.20240330 (the latest was available at the time of the issue creation).

⏯ Playground Link

Playground here

💻 Code

1. // Setup
2. type ExampleType = { property: boolean };
3. let reference: ExampleType = { property: true };
4. 
5. // The type that introduces the unexpected behavior.
6. type CopyType<T> = {
7.   [P in keyof T]: T[P];
8. };
9. 
10. // Check if the assignment works.
11. let check: CopyType<ExampleType> = { property: true };
12. 
13. // Test with generic constraints.
14. function test<T extends ExampleType>(param: CopyType<T>) {
15.   param.property = true; // It compiles.
16.   param.anotherProperty = true; // It does not compile, as expected.
17.   param = { property: true }; // It does not compile, not expected.
18.   let check: CopyType<T> = { property: true }; // It does not compile, not expected.
19. }

🙁 Actual behavior

The compiler partially infers the type: it doesn't compile as expected when I do the assignment param.anotherProperty = true. This makes me think that there is some understanding of the type thanks to the generic constraint.
In the other two lines (17 and 18) it doesn't compile with the corresponding error:

  • Type '{ property: true; }' is not assignable to type 'CopyType'.

🙂 Expected behavior

I would expect a consistent behavior:

  • If it should be capable of inferring the type, that the code compiles.
  • If something is incorrect in that definition, that also line 16 doesn't compile.

Additional information about the issue

I came out with this example from an unexpected behavior using ts-essential, that has more unexpected behaviors:

// Setup
type ExampleType = { propertyA: boolean; propertyB: number };
type KeepBooleanProperties<T> = PickProperties<T, boolean>;
let check1: KeepBooleanProperties<ExampleType> = { propertyA: true }; // Compiles, expected.
let check2: KeepBooleanProperties<ExampleType> = { propertyA: true, propertyB: 1 }; // It doesn't compile, expected.

// Test with generic constraints.
function test<T extends ExampleType>(param: KeepBooleanProperties<T>) {
  param.propertyA = true; // It doesn't compile, not expexted.
  param.anotherProperty = true; // It does not compile, expected.
  param = { propertyA: true }; // It does not compile, not expected.
  let check: CopyType<T> = { property: true }; // It does not compile, not expected.
@whzx5byb
Copy link

I don't see failure here, and it's nothing to do with CopyType<T>. Just consider:

function test<T extends ExampleType>(param: T) {
    param = { property: true };
}

You can't assign {property: true} to T here because T could be a subtype with more properties.

@DanielBertocci
Copy link
Author

DanielBertocci commented Mar 31, 2024

Thank you @whzx5byb you are right. I think the example I have built in my initial post is not effective. I missed probably the key point that is visible in the section "Additional information". I think the following version is the proper example, where I have unrolled ts-essential:

Playground here

// Setup
type ExampleType = { propertyA: boolean; propertyB: number };

type BooleanKeys<T> = {
  [Key in keyof T]: T[Key] extends boolean ? Key : never;
}[keyof T];
let booleanKeysCheck1: BooleanKeys<ExampleType> = 'propertyA'; // Compiles, expected.
let booleanKeysCheck2: BooleanKeys<ExampleType> = 'propertyB'; // It doesn't compile, expected.

type KeepBooleanProperties<T> = Pick<T, BooleanKeys<T>>;
let check1: KeepBooleanProperties<ExampleType> = { propertyA: true }; // Compiles, expected.
let check2: KeepBooleanProperties<ExampleType> = { propertyA: true, propertyB: 1 }; // It doesn't compile, expected.

// Test with generic constraints.
function test<T extends ExampleType>(param: KeepBooleanProperties<T>) {
  param.propertyA = true; // <--------- It doesn't compile, not expexted.
  param.propertyB = 1; // It does not compile, expected.
  param = { propertyA: true }; // It does not compile, expected after comment of whzx5byb.
}

@jcalz
Copy link
Contributor

jcalz commented Mar 31, 2024

What if T is {propertyA: false, propertyB: 0}? Then you're assigning true to false. Even if you "fix" this you'll just be taking advantage of the hole (#28952 (comment)) that lets you assign properties directly. I don't see anything here that isn't a combination of existing missing features, design limitations, intentional tradeoffs. Your BooleanKeys confuses the compiler because of the lack of #48992, and assignments to generics with nongeneric values are often problematic, see Why can't I return a generic 'T' to satisfy a Partial?.

@DanielBertocci
Copy link
Author

DanielBertocci commented Mar 31, 2024

Really insight full. I haven't considered that case and it makes perfectly sense. Let's take a step aside and avoid assignments.
I see that my case comes from the similar need of #48992. But what about the following (and more compact) example?

Playground here

// Setup
type ExampleType = { propertyA: boolean; propertyB: number };
type A<T> = {[Key in keyof T]: Key}[keyof T];

let check1: A<ExampleType> = 'propertyA';
let check2: A<ExampleType> = 'propertyB';

// Test with generic constraints.
function test<T extends ExampleType>(param: A<T>) {
  if (param.propertyA) { // It doesn't compile, expected known property, with unknown type.
    // Logic...
  }

  if (param['propertyA']) { // It doesn't compile, expected known property, with unknown type.
    // Logic...
  }
}

I understand that there are many cases about types, so after the example of @jcalz I understand that there are problems deriving types. But It seems to me that the access to that property is not ambiguous, where I could use a type guard function, for example.
I also took the distances from the need of filtering the properties by type.

@RyanCavanaugh
Copy link
Member

In this example, a legal call is

test('propertyA');

I don't understand why we would be expected to be able to access the property .propertyA on a string. It's not there.

@RyanCavanaugh RyanCavanaugh added the Question An issue which isn't directly actionable in code label Apr 1, 2024
@DanielBertocci
Copy link
Author

Yes I messed up the previous example. I did several attempts, and this is the latest example I have:

Playground here

// Setup
type ExampleType = { propertyA: boolean; propertyB: number };
type PickProperties<Type, Value> = Pick<Type, {
  [Key in keyof Type]: Type[Key] extends Value ? Key : never;
}[keyof Type]>;
let check1: PickProperties<ExampleType, boolean> = { propertyA: true }; // Compiles, expected.
let check2: PickProperties<ExampleType, boolean> = { propertyA: true, propertyB: 1 }; // It doesn't compile, expected.

// Test with generic constraints.
function test<T extends ExampleType>(param: PickProperties<T, boolean>) {
  if(param.propertyA){  // It doesn't compile, not expected.
    // Logic
  }
}

test({propertyA: true}); // Is there any way to call test without the property "propertyA"?

This though is about the same topic mentioned by @jcalz, because I filter the properties based on a type.
That means I will wait for updates on the issues mentioned. Thanks for the support.

@typescript-bot
Copy link
Collaborator

This issue has been marked as "Question" 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 Apr 4, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Question An issue which isn't directly actionable in code
Projects
None yet
Development

No branches or pull requests

5 participants