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

in operator: check key/property is valid (using keyof) #50953

Closed
4 of 5 tasks
OliverJAsh opened this issue Sep 26, 2022 · 10 comments
Closed
4 of 5 tasks

in operator: check key/property is valid (using keyof) #50953

OliverJAsh opened this issue Sep 26, 2022 · 10 comments
Labels
Declined The issue was declined as something which matches the TypeScript vision Suggestion An idea for TypeScript

Comments

@OliverJAsh
Copy link
Contributor

OliverJAsh commented Sep 26, 2022

Suggestion

πŸ” Search Terms

  • in operator
  • keyof
  • key
  • property
  • validate

βœ… Viability Checklist

My suggestion meets these guidelines:

  • This wouldn't be a breaking change in existing TypeScript/JavaScript code
  • This wouldn't change the runtime behavior of existing JavaScript code
  • This could be implemented without emitting different JS based on the types of the expressions
  • This isn't a runtime feature (e.g. library functionality, non-ECMAScript syntax with JavaScript output, new syntax sugar for JS, etc.)
  • This feature would agree with the rest of TypeScript's Design Goals.

⭐ Suggestion

When using the in operator K in T, TypeScript should error if the provided key K is not assignable to keyof T.

πŸ“ƒ Motivating Example

const obj = { a: "", b: "" };

// This condition will always be false
const condition = "c" in obj;
declare const obj:
    | {
          name: string;
          age: string;
      }
    | {
          species: string;
      };

// This is a mistake, but it's not caught by the compiler.
const condition = 'nam' in obj;

πŸ’» Use Cases

What shortcomings exist with current approaches?

I've tried to define a has function for this purpose but there are lots of edge cases: https://stackoverflow.com/questions/58749852/typescript-defining-the-in-operator-as-a-function

@MartinJohns
Copy link
Contributor

Note that the in operator is explicitly mentioned as a way to narrow union types in the handbook: https://www.typescriptlang.org/docs/handbook/2/narrowing.html#the-in-operator-narrowing

@RyanCavanaugh RyanCavanaugh added Suggestion An idea for TypeScript Declined The issue was declined as something which matches the TypeScript vision labels Sep 26, 2022
@RyanCavanaugh
Copy link
Member

This is opposite to the highly-requested behavior requested in #21732, which we just implemented

const obj = { a: "", b: "" };
if("c" in obj) {
  // Works in 4.9 nightly
  obj.c
}

With exact types we could revisit, but we don't have exact types

@RyanCavanaugh RyanCavanaugh closed this as not planned Won't fix, can't repro, duplicate, stale Sep 26, 2022
@OliverJAsh
Copy link
Contributor Author

@MartinJohns This suggestion isn't going against that, as far as I can see. The narrowing is useful. I just want TS to check that it's a valid key in at least one of the union members, so I can't make silly mistakes.

@RyanCavanaugh I think #21732 is really useful, when we're dealing with a type like unknown or object, but in this case we already have type information about the properties (assuming that the type is exact like you say).

@RyanCavanaugh
Copy link
Member

If the object isn't aliased in some way, why are you writing an in check?

@OliverJAsh
Copy link
Contributor Author

Here's one example:

type Fish = { swim: () => void };
type Bird = { fly: () => void };
type Human = { swim?: () => void; fly?: () => void };

const getType = (animal: Fish | Bird | Human) => {
    // oops, typo!
    if ("flyyy" in animal) {
        return "bird";
    } else {
        return "other";
    }
};

@MartinJohns
Copy link
Contributor

The narrowing is useful. I just want TS to check that it's a valid key in at least one of the union members, so I can't make silly mistakes.

You explicitly mentioned keyof T, which only includes keys common in all union types.

@OliverJAsh
Copy link
Contributor Author

You're right. I should have said something like "distributive keyof" (type DistributiveKeys<T> = T extends any ? keyof T : never).

@RyanCavanaugh
Copy link
Member

I think #21732 is really useful, when we're dealing with a type like unknown or object [...]

Sure, but what you're proposing breaks subtyping linearity: if k in T is valid when T is unknown (or object), it doesn't make sense to say that k in T' is invalid when T' is some subtype of T (which, for unknown, is by definition). There are some places where breaking that linearity makes sense, but they're pretty few and far between IMO.

If you had written

type Fish = { swim: () => void };
type Bird = { fly: () => void };
type Human = { swim?: () => void; fly?: () => void };

const getType = (animal: Fish | Bird | Human) => {
    if ("superman" in animal) {
        return "kryptonian";
    } else {
        return "earthling";
    }
};

there doesn't seem to be an error in this program.

@MartinJohns
Copy link
Contributor

@OliverJAsh You could probably easily write an ESLint rule for this, if you still want it.

@LexRiver
Copy link

LexRiver commented Dec 9, 2024

there doesn't seem to be an error in this program.

Can you explain why?
In Javascript there is no error, but in Typescript we expecting only types Fish | Bird | Human, not an extensions of these types, so we must work with known properties of these types.
If we expecting superman in this function we should write animal: Fish | Bird | Human | Superman

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Declined The issue was declined as something which matches the TypeScript vision Suggestion An idea for TypeScript
Projects
None yet
Development

No branches or pull requests

4 participants