Skip to content

Rule proposal: comment-based array exhaustiveness check #7774

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
6 tasks done
Retsam opened this issue Oct 19, 2023 · 8 comments
Closed
6 tasks done

Rule proposal: comment-based array exhaustiveness check #7774

Retsam opened this issue Oct 19, 2023 · 8 comments
Labels
enhancement: new plugin rule New rule request for eslint-plugin package: eslint-plugin Issues related to @typescript-eslint/eslint-plugin wontfix This will not be worked on

Comments

@Retsam
Copy link
Contributor

Retsam commented Oct 19, 2023

Before You File a Proposal Please Confirm You Have Done The Following...

My proposal is suitable for this project

  • My proposal specifically checks TypeScript syntax, or it proposes a check that requires type information to be accurate.
  • My proposal is not a "formatting rule"; meaning it does not just enforce how code is formatted (whitespace, brace placement, etc).
  • I believe my proposal would be useful to the broader TypeScript community (meaning it is not a niche proposal).

Description

This rule is rooted in a question that comes up from time-to-time on TS-based help servers, people write code like this:

type Colors = "Red" | "Blue" | "Green" | "Fuchsia";

const allColors: Color[] = ["Red"];

and ask "how can I get a type error on colors if it doesn't include all the colors?". The answer is, as far as the type-checker is concerned, is more-or-less "you can't". (Barring some truly dark magic that converts the union to a tuple and tries to generate all permutations) And I can't really imagine the language adding a check for this.

But this is the sort of thing that could be opted into via linting:

type Colors = "Red" | "Blue" | "Green" | "Fuchsia";
// @ensure-exhaustive
const allColors: Color[] = ["Red"]; // could raise an eslint error here

The above example is a bit contrived, you could generate the type from the array using as const (though it's a bit of an advanced technique). Here's a less silly example:

type Person = { name: string, age: number };
// @ensure-exhaustive
const personKeys: Array<keyof Person> = ["name" , "age"];

This can be useful for iterating over an object in a type-safe way: iterating Object.keys is not safe (due to excess keys) and requires extra casting to use, and in some cases the list of keys is useful absent an actual instance of the object to generate it from.

As an example of prior-art, the ts-transformer-keys is a tool people have used to solve this problem in the past (though it requires a non-standard TS runtime), though it

I think a minimum (but still useful) set of rules would be:

  1. This rule would only apply in the presence of a specific "directive" comment, e.g. // @ensure-exhaustive
  2. The comment must appear before the assignment of an array to a const variable with an explicit type annotation
  3. Supported types would be a union of primitive values (including enums)

Both #2 and especially #3 could be expanded to support more patterns and types (e.g. discriminated unions, perhaps); but I think this minimal set would cover most usages.


This is somewhat niche

Fail Cases

type Colors = "Red" | "Blue" | "Green" | "Fuchsia";
// @ensure-exhaustive
const allColors: Color[] = ["Red"]; // could raise an eslint error here

enum SomeLetters { X, Y, Z }
// @ensure-exhaustive
const allSomeLetters = [ SomeLetters.X, SomeLetters.Y ] 

type Person = { name: string, age: number };
const 

/* Would also error on misuse of the directive, e.g.: */

// @ensure-exhaustive
const arr = [1, 2, 3]; // no type annotation, can't check that it's exhaustive

Pass Cases

type Colors = "Red" | "Blue" | "Green" | "Fuchsia";
// No comment, no error
const allColors: Color[] = ["Red"];

enum SomeLetters { X, Y, Z }
// @ensure-exhaustive
const allSomeLetters = [ SomeLetters.X, SomeLetters.Y, SomeLetters.Z ]

Additional Info

A bit of evidence that this is a somewhat common request, here's a bunch of times people have asked about this on the Typescript Discord:

https://discord.com/channels/508357248330760243/508357248330760249/1154068959289679973
https://discord.com/channels/508357248330760243/1111330767801438318/1111330767801438318
https://discord.com/channels/508357248330760243/1153379072290869368/1153386393532375181
https://discord.com/channels/508357248330760243/1103350513510121522/1103351629790580766
https://discord.com/channels/508357248330760243/740274647899308052/860400499643121684

Not covered by this limited version of the proposal, would require a discriminated union mode:
https://discord.com/channels/508357248330760243/873402624072880149/923683183210397747

@Retsam Retsam added enhancement: new plugin rule New rule request for eslint-plugin package: eslint-plugin Issues related to @typescript-eslint/eslint-plugin triage Waiting for team members to take a look labels Oct 19, 2023
@JoshuaKGoldberg JoshuaKGoldberg added evaluating community engagement we're looking for community engagement on this issue to show that this problem is widely important and removed triage Waiting for team members to take a look labels Oct 19, 2023
@JoshuaKGoldberg
Copy link
Member

somewhat niche

...yes 😄. This is definitely useful for that niche group. But it's niche nonetheless. Marking as evaluating community engagement to see if there's a strong demand for this kind of rule to be added to typescript-eslint itself.

In the meantime, my advice would be to try it out in a third party plugin made using the APIs documented at https://typescript-eslint.io/developers/custom-rules. You can iterate with variants of the comments, core functionality, options, etc. without having to deal with our versioning and breaking changes.

@Josh-Cena
Copy link
Member

Why don't people generate types from values, though?

const allColors = ["Red"] as const;
type Colors = typeof allColors[number];

@bradzacher
Copy link
Member

bradzacher commented Oct 19, 2023

Yeah there's also this other workaround if you really want a union-type-first:

type Colors = "Red" | "Blue" | "Green" | "Fuchsia";
const colorsKeys: Record<Colors, null> = {
    Red: null,
    Blue: null,
    Green: null,
    Fuchsia: null,
};
const allColors = Object.keys(colorsKeys) as Colors[];

TS will error if colorsKeys does not include all the members of the union - which thereby enforces allColors is exhaustive.

Also works for the object case:

type Person = { name: string, age: number };
const personKeys: Array<keyof Person> = Object.keys(
  { name: null, age: null } satisfies Record<keyof Person, null>
) as (keyof Person)[];

There's also methods to convert a union type to a tuple type - but they're pretty unstable right now. It does work though if you want to start with a union type. Just not recommended.

@Retsam
Copy link
Contributor Author

Retsam commented Oct 20, 2023

@bradzacher Yeah, I figured that would likely be the answer but wanted to open the ticket anyway.

@Josh-Cena I listed the simple Colors case mostly as a minimal example - I do prefer the as const approach (and mentioned it in the original post), but it does rely on the type being local to your code, (not e.g. provided a library), and is much more awkward with enums (which a lot of people insist are the 'correct', way to define these kind of things) and objects. (The workaround bradzacher shows... well I understand them, but I suspect I'd have a hard time getting those sort of patterns through code review)

There's also methods to convert a union type to a tuple type - but they're pretty unstable right now. It does work though if you want to start with a union type. Just not recommended.

I referenced these as "dark magic" in my original post, and I think they'll always be "unstable" because tuples are ordered and unions are fundamentally not. Changing the order of types in a union shouldn't break code.

@Josh-Cena
Copy link
Member

FWIW, this seems very unlike any ts-eslint rule we have. It's not "enforcing a style" or "catching potential issues", but solely "requesting the type checker to check for a specific variable declaration using a narrower type than actually declared", if that makes sense. For one thing, we've never assigned semantic meaning to comments, other than opting out from checks.

@bradzacher
Copy link
Member

Yeah in general we don't want to delve into adding lint rules that rely on comments existing.

Comments are a cumbersome thing to track and attach and are a clunky way to do things for a general-purpose plugin like ours.
It would lead to us creating a new standard in doing things which we'd like to avoid pioneering.

It's a different story for a smaller 3rd party plugin to do because it's like "if you want this you can adopt this plugin and the standard it introduces" but when if we were to do it it would come across more as "this is the blessed way to do it in TS", given our size and relationships with the TS team. This is obviously not something we want to do!

@Josh-Cena
Copy link
Member

See also: microsoft/TypeScript#53171

@JoshuaKGoldberg
Copy link
Member

Ok, yes, agreed up: this isn't ready for typescript-eslint core. I was too optimistic at first 🙂.

Again, would love to see a plugin built for this rule! Just we can't accept it in this repository without standardization in the community.

Thanks for filing!

@JoshuaKGoldberg JoshuaKGoldberg closed this as not planned Won't fix, can't repro, duplicate, stale Oct 23, 2023
@JoshuaKGoldberg JoshuaKGoldberg added wontfix This will not be worked on and removed evaluating community engagement we're looking for community engagement on this issue to show that this problem is widely important labels Oct 23, 2023
@github-actions github-actions bot locked as resolved and limited conversation to collaborators Oct 31, 2023
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
enhancement: new plugin rule New rule request for eslint-plugin package: eslint-plugin Issues related to @typescript-eslint/eslint-plugin wontfix This will not be worked on
Projects
None yet
Development

No branches or pull requests

4 participants