Search code examples
typescripttype-safetyconditional-types

Why isn’t my conditional key remapping filtering out keys with attribute: true?


I’m trying to create a mapped type that filters out object keys based on the value of a boolean property in a class. I have the following code:

class Test {
  readonly attribute: boolean;

  constructor(attribute?: boolean) {
    this.attribute = attribute ?? true;
  }
}

type TestType2<T extends Record<string, Test>> = {
  [K in keyof T as T[K]['attribute'] extends true ? never : K]: number;
};

const a = {
  test1: new Test(),
  test2: new Test(false),
};

type FinalTest = TestType2<typeof a>;

I expected FinalTest to be { test2: number } since test1 is constructed with the default attribute (i.e., true), and only test2 (with attribute equal to false) should remain. However, the resulting type is { test1: number, test2: number }.

Why does the conditional type T[K]['attribute'] extends true ? never : K not filter out test1 as expected? Is it an issue with how the class property is typed, or is there another approach to achieve the desired filtering? Any insights would be appreciated!

Thank you!


Solution

  • The reason test1 is not filtered out is that the type of attribute is evaluated at the time of type definition:

    class Test {
      readonly attribute: boolean;
      // ...
    }
    

    At that time, attribute is simply of type boolean, which means TypeScript cannot determine whether it is true or false based on the constructor's input. Which means, no keys will be filtered out because boolean cannot extend true or false.

    To achieve the desired filtering, you can use generic types like this:

    class Test<Attr extends boolean = true> {
      readonly attribute: Attr;
    
      constructor(attribute: Attr = true as any) {
        this.attribute = attribute;
      }
    }
    
    type TestType2<T extends Record<string, Test<boolean>>> = {
      [K in keyof T as T[K]['attribute'] extends true ? never : K]: number;
    };
    
    const a = {
      test1: new Test(),
      test2: new Test(false),
    };
    
    type FinalTest = TestType2<typeof a>; // { test2: number }
    

    TS Playground for the solution

    Now, the type of attribute is evaluated when you instantiate the Test class. If you don't pass any argument to the constructor, the type will default to true; otherwise, it will be inferred from the provided attribute. This allows TypeScript to correctly filter out keys based on the actual value of attribute.

    Just make sure that the Attr default type matches the attribute default value.