Skip to content

Consider disallowing in operator use with arbitrary key operands on closed types for which the keys are known #59299

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

Open
6 tasks done
loucadufault opened this issue Jul 16, 2024 · 6 comments
Labels
Awaiting More Feedback This means we'd like to hear from more people who would be helped by this feature Suggestion An idea for TypeScript

Comments

@loucadufault
Copy link

loucadufault commented Jul 16, 2024

πŸ” Search Terms

in operator, type guard, known property/props, closed type, rename property

βœ… Viability Checklist

⭐ Suggestion

The in operator serves as a type guard against record types: #10485

As it is implemented, it allows checking for the presence of arbitrary keys on an object.

The suggestion would be to disallow using the in operator with key names that not one of the known keys in the object's type. This would prevent developer errors where the key for which the presence is checked is not one of the keys that was explicitly declared to the type system, and might for example contain a typo.

declare const foo: { someProp: number }

if ('a' in foo) { // type error: Property 'a' does not exist on type '{ someProp: number; }'.
  ...
}

This could be guarded behind a compiler option, so as to not introduce a breaking change.

Of course, the in operator has a wide range of uses, especially for type guarding property accesses on objects of completely unknown types, so these should be preserved:

declare const foo: unknown
declare const bar: Record<string, number>
declare const baz: { someProp: number; [k in string]: number }

 // in allowed against unknown, any, and other "open" record types or types with an index signature
if ('a' in foo || 'a' in bar || 'a' in baz) {
  ...
}

For cases where the original type of the variable was not permissive enough, but the programmer knows better, we can allow checking for the presence of the key with an explicit cast:

const foo = { someProp: 123, a: 'hello' }
const bar: { someProp: number } = foo

if ('a' in bar as unknown) { // valid, with the explicit cast
  bar // type is inferred the same as currently
  // ^? { someProp: number } & { a: unknown; } 
}

πŸ“ƒ Motivating Example

Currently, I can write a valid program to discriminate a union:

type Puppy = {
  color: string
}

declare const foo: Puppy | { someProp: string }

if ('color' in foo) {
  console.log(foo.color)
}

Now, let's say my British colleague has a pass at the code, renaming variable names to UK English:

type Puppy = {
  colour: string // change made
}

declare const foo: Puppy | { someProp: string }

// elsewhere in the program
if ('color' in foo) { // change forgotten
  console.log(foo.color)
}

the program remains valid, yet TypeScript is unable to provide any indication that the change had consequences.

With the proposed feature:

type Puppy = {
  colour: string // change made
}

declare const foo: Puppy | { someProp: number }

// elsewhere in the program
if ('color' in foo) { // type error: Property 'color' does not exist on type 'Puppy | { someProp: number; }'.
  console.log(foo.color)
}

if ('color' in foo as unknown) { // valid, with the explicit cast
  console.log(foo.color)
}

πŸ’» Use Cases

  1. What do you want to use this for?
    Preventing developer errors (typos), allowing for safe property renames.

  2. What shortcomings exist with current approaches?
    Does not warn of checks for presence of properties which are unknown to the type system (to allow for checking of properties not represented in the type system). This is especially evident when using the operator to discriminate a union, as the in operator key operand is always intended to be a known property in such cases.

  3. What workarounds are you using in the meantime?
    https://stackoverflow.com/questions/70670913/type-safe-in-type-guard

@loucadufault loucadufault changed the title Consider disallowing in operator use on closed type aliased of a known type Consider disallowing in operator use on closed types for which the keys are known Jul 16, 2024
@loucadufault loucadufault changed the title Consider disallowing in operator use on closed types for which the keys are known Consider disallowing in operator use with arbitrary key operands on closed types for which the keys are known Jul 16, 2024
@MartinJohns
Copy link
Contributor

[x] This wouldn't be a breaking change in existing TypeScript/JavaScript code

Are you sure?

This sounds like a duplicate of #50953.

@loucadufault
Copy link
Author

[x] This wouldn't be a breaking change in existing TypeScript/JavaScript code

Are you sure?

See OP:

This could be guarded behind a compiler option, so as to not introduce a breaking change.

@RyanCavanaugh RyanCavanaugh added Suggestion An idea for TypeScript Awaiting More Feedback This means we'd like to hear from more people who would be helped by this feature labels Jul 16, 2024
@leumasic
Copy link

Please consider. This is a nasty source of bugs.

@LexRiver
Copy link

LexRiver commented Dec 9, 2024

If typescript knows that this property doesn't exists in this type, there should be an error.

type TypeA = {a:string}
type TypeB = {b:string}

function func(param1:TypeA|TypeB){
    if('c' in param1){ // here should be an compilation error
        
    }
}

The workaround for this kind of bugs is pretty complicated:
https://stackoverflow.com/questions/70670913/type-safe-in-type-guard

@loucadufault
Copy link
Author

I've edited the issue to include the above SO link as a possible workaround.

@LexRiver
Copy link

Another workaround is

if('someProperty' satisfies keyof MyType in myVariable){

}

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Awaiting More Feedback This means we'd like to hear from more people who would be helped by this feature Suggestion An idea for TypeScript
Projects
None yet
Development

No branches or pull requests

5 participants