Detecting Enum & String Literal Union Changes
Hey everyone, let's dive into some pretty cool updates for API Extractor that will make tracking changes in your enums and string literal unions way more granular. You know how sometimes you add a new member to an enum, or maybe a new string option to a union type, and API Extractor just gives you a generic "type widened" message? Well, those days are about to be over, guys! We're rolling out specific change categories to make it super clear exactly what changed. This is all part of a bigger effort, building on #127, to give you the best possible insight into your API surface.
Why This Matters: Granular Change Detection
So, what's the big deal here? Basically, we're refining how API Extractor's comparator works. Instead of lumping various type changes into a single, broad category like type-widened, we're introducing more specific change types. This means when you add a new member to an enum, or expand a string literal union, you'll get a distinct enum-member-added notification. This is huge for understanding breaking changes or just keeping track of your API's evolution. It’s like going from a generic “something changed” to “this specific part changed,” which is way more actionable. We're also tackling changes to the @enumType tag, which dictates whether an enum is considered open or closed. This distinction is important for consumers of your API, especially when it comes to exhaustive type checking. By detecting when an enum type opens (meaning it might have new members added later) or closes (meaning its members are now considered fixed), we provide clearer guidance.
This update means that when comparing different versions of your TypeScript code, API Extractor will be smarter. It won't just say, "Hey, this enum got bigger." It'll say, "Hey, this enum got bigger because you added a new member." And for string literal unions, it’ll be equally specific: "This union now includes a new string literal." This level of detail is invaluable for maintaining robust APIs and ensuring your consumers aren't caught off guard by unexpected type expansions. It helps automate documentation updates and informs consumers about the precise nature of changes, rather than making them guess.
Under the Hood: How We're Doing It
Let's peek under the hood a bit, shall we? The core of this update involves enhancing the analyzeTypeChange function, or potentially creating new, dedicated functions to handle these specific scenarios. The goal is to make the comparator smarter and more sensitive to particular kinds of type modifications.
1. Detecting Enum Member Additions
First up, detecting enum member additions. When the comparator looks at two versions of an enum, it needs to be able to say, "Yep, a new member popped up here!" Instead of just flagging it as a generic type-widened change, we'll be emitting a new category: enum-member-added. This involves writing logic, perhaps in a function like detectEnumMemberAddition, that specifically compares the members of an old enum type against a new one. If it finds members present in the new type that weren't in the old, it’ll return true, signaling that we’ve got an addition. This specificity is key. It’s not just about the size of the enum changing; it’s about how it changed. This applies to both regular enums and const enum declarations, giving you consistent detection across the board. We'll be looking at the ts.Type objects for both the old and new symbols to perform this comparison. The logic will need to carefully identify what constitutes an enum and then iterate through its members to spot any new entries.
2. Catching String Literal Union Expansions
Similarly, we need to handle string literal union expansion. Think of types like 'GET' | 'POST'. If you update this to 'GET' | 'POST' | 'PUT', that’s an expansion. We'll have a function, say detectUnionExpansion, that compares two ts.Type objects. For string literal unions, it'll essentially compare the sets of literal strings. It can then tell us exactly which members were added and which were removed. When new members are added and no members are removed, we'll categorize this specifically. This is crucial because adding new string literals to a union is often a non-breaking change for existing code, but it’s still a change API consumers need to be aware of. The function will return a structure detailing the added and removed members, allowing us to make informed decisions about the change category.
3. Emitting the enum-member-added Category
Now, how do we actually emit these changes? When our checks confirm that an enum or a string literal union has gained members without losing any, we'll emit the enum-member-added category. The code snippet shows a conditional check: if the symbol kind is 'enum' or if it's a string literal union, we call detectUnionExpansion. If that function returns an expansion where added.length > 0 and expansion.removed.length === 0, boom – we emit enum-member-added. This ensures that only genuine expansions trigger this specific category, keeping our change tracking accurate and helpful. This logic acts as a gatekeeper, making sure we only apply this new category when the conditions are precisely met.
4. Tracking @enumType Tag Changes
Another significant piece of this puzzle is tracking changes to the @enumType tag. This metadata tag can specify whether an enum is treated as 'open' or 'closed'. An 'open' enum suggests that new members could be added later, discouraging exhaustive matching by consumers. A 'closed' enum implies its members are fixed. We'll introduce detectEnumTypeChange to compare the metadata of old and new symbols. If an enum goes from 'closed' to 'open', we emit enum-type-opened. If it goes from 'open' to 'closed', it's enum-type-closed. This gives consumers clear signals about whether they can rely on exhaustive checks for an enum. The default behavior is 'closed', so we only flag explicit changes. This is super important for API stability and correctness, guiding developers on how to safely interact with enums.
5. Explaining the New Categories
Finally, what good are new categories without clear explanations? We'll update the generateExplanation() function to provide human-readable descriptions for these new change types. For enum-member-added, it'll clearly state that the enum or union added new member(s) and provide the 'before' and 'after' signatures. For enum-type-opened, it'll explain that an enum changed from closed to open, cautioning against exhaustive matching. Conversely, for enum-type-closed, it'll inform that an enum changed from open to closed, indicating that exhaustive matching is now safe. These explanations are vital for ensuring that API consumers understand the implications of the detected changes immediately. They turn raw change data into actionable insights, making API evolution smoother and more transparent for everyone involved.
Testing, Testing, 1, 2, 3!
To make sure all this works flawlessly, we've got a comprehensive set of test cases. We need to verify:
- Enum Member Addition: Does adding a member (or multiple members) to a regular or
const enumcorrectly emitenum-member-added? We also need to ensure that removing members still triggerstype-narrowed, and that combined add/remove scenarios don't mistakenly triggerenum-member-added. Changing a member's value without adding or removing should still result intype-narrowed. - String Literal Union Expansion: Does adding a new string literal to a union like
'a' | 'b'to become'a' | 'b' | 'c'correctly emitenum-member-added? Again, removals should triggertype-narrowed, and combined changes handled appropriately. @enumTypeChange Detection: We need to test all transitions: unmarked to open, closed to open, open to unmarked, open to closed. We also need to confirm that no change is emitted when the state remains the same (closed to closed, open to open, unmarked to unmarked).- Combined Changes: What happens when an enum member is added and an
@enumTypetag changes? We expect both changes to be reported separately. - Explanations: Finally, are the generated explanations clear, accurate, and informative for each new category? Do they include the necessary before/after details?
Dependencies and What's Next
This work depends on some foundational changes already in progress, specifically #128 (Core Type System Changes) and #130 (Parser Integration). Once these pieces are in place, we'll be able to move forward with classifier changes and policy updates that build upon this enhanced comparator functionality.
Acceptance Criteria: The Final Checklist
To wrap it all up, here’s our acceptance criteria – the things that absolutely must be true for this feature to be considered complete:
- Enum member additions consistently emit
enum-member-added. - String literal union expansions also correctly emit
enum-member-added. @enumTypetag changes result in the appropriateenum-type-openedorenum-type-closedcategories.- All generated explanations are crystal clear and provide valuable context.
- And, of course, all the test cases we outlined pass with flying colors!
This is going to be a fantastic improvement for anyone using API Extractor, providing much-needed clarity on changes to these common TypeScript constructs. Stay tuned for more updates!