Microsoft/TypeScript

Mapped type with enum keys emits string keys in d.ts type, but uses enum keys in .ts type

Open

#60,247 建立於 2024年10月17日

在 GitHub 查看
 (3 留言) (0 反應) (0 負責人)TypeScript (6,726 fork)batch import
BugDomain: Declaration EmitHelp Wanted

倉庫指標

Star
 (48,455 star)
PR 合併指標
 (平均合併 6天 17小時) (30 天內合併 9 個 PR)

描述

🔎 Search Terms

mapped type, enum, declaration

🕗 Version & Regression Information

  • This is the behavior in every version I tried

⏯ Playground Link

https://www.typescriptlang.org/play/?ts=5.6.3&ssl=25&ssc=36&pln=25&pc=1#code/KYOwrgtgBAYg9nKBvAUFKBBKBeKByAQzwBo0oAhHfAIxJQF8UATYAYwBsCAnYKAMzAhWAFwCWcEPwQAeACoA+ABTcA5gC4osgJQbZBFSuBMAqiHEg58gNwoUwgJ4AHXnoNHT5y1SRQA2gGkoUUkAa2B7OD5NAF0NHzD7DX9iKAA3AnYwYF0A6Kh6fN8EyJibYAAPRzguYShWCQBnWuFgJqo+BEVUdF94OAA6DFj8Oh6+-vJhgEZSei0bFAB6RahhAAteBKgGgjEGvlFWqTgURRam-q2dvYOjvvmllYIQJjrn7YB3UWFWNagv9ZwMC1YBcLjVFANAG-KBnVrCS7hLTIMisAgNXjjDAaag8AghGzoNEY2AICY4vEEsgsPgEMDsYRqMjoZZQQCg5NsCHxgMzYedatdRPtDg0oCBgKlQQ9GLZFgAqOVQAC0KtVavVGs1UDli1sj1WayFQVFYAawRUBt4hnFXF2RlWTiOfHB0HWvH6TH6wgaKQAVqbajwQAQIPbhIgCKk4KJXhx0Ws7I7NPpDCYzBIACKyADKXlw3XQfkCwSgxSismGBcLhYSSUJ1cL6Uy2U0uXr6HoNnoRXCJQrZUq1VqLDjPDqjWa8Kz2d0Kfc6ZA0+kVagBA0TS45vbUGoGnAEGooK71llKzdpfCUCYcCOIDgAt2Qr49lJJzhTWniJfguFdwQD1ZZ5Y3eKFvhhAE1iBEEwQhUCfj+d9hE-BJkQLYlMTJbEd0pet0NfclsOAfF6xpOkGSZatWUAGXIoFBcEuAaXlEM5G4RTFCUpS7IA

💻 Code

enum Foo {
  A = 'a',
  B = 'b',
}
declare function foo<T>(arg: T): TaggedUnion<T>;

type TaggedUnion<T> = { [K in keyof T]: { key: K, value: T[K] } }[keyof T];
export const test = foo({
  [Foo.A]: '',
  [Foo.B]: 1,
});

// the key satisfies foo
(test.key satisfies Foo);
// and can switch without error
switch (test.key) {
  case Foo.A: break;
  case Foo.B: break;
  default:
    // ✅ safe
    (test satisfies never);
}


/** ---------------------------- */


// this is using the generated types from the .d.ts, just renamed to avoid clash
type TaggedUnionDTS<T> = {
    [K in keyof T]: {
        key: K;
        value: T[K];
    };
}[keyof T];
export declare const testDTS: TaggedUnionDTS<{
    a: string;
    b: number;
}>;

// the key does not satisfy Foo
(testDTS.key satisfies Foo);
// and can switch without error
switch (testDTS.key) {
  case Foo.A: break;
  case Foo.B: break;
  default:
    // ❌ errors
    (test satisfies never);
}

🙁 Actual behavior

When the above code is used and referenced directly via .ts files (i.e. in the IDE) then it all works as expected. TS is able to understand that the .key property is Foo and so it understands that the switch refines the variable and so in the default the variable correctly refines to never.

However when you consume the code via the .d.ts output - the relationship with Foo is lost entirely and so the same switch now is no longer able to refine the variable and so in the default the variable has not refined at all.

🙂 Expected behavior

Either .d.ts code should include the relationship with Foo, or the .ts code should not have a relationship to Foo. I.e. the behaviour should be consistent between the two.

Additional information about the issue

An engineer encountered this at Canva. The test variable was declared in a file that was previously within the same project as the switch. During a refactor they moved the test variable to a new project, causing the CI typecheck to switch from using the .ts directly to using the .d.ts.

This caused the switch to not refine, causing a TS error on the default case.

貢獻者指南