Search code examples
typescriptgenericstypesgeneric-programmingtypescript-generics

How to filter record keys based on their values?


I have this record:

interface TheRecord extends TheRecordType {
  a: { typeA: 'string' },
  b: { typeB: 123 },
  c: { typeA: 'string' },
}

type TheRecordType = Record<string, TypeA | TypeB>

type TypeA = { typeA: string }
type TypeB = { typeB: number }

I want my function to accept only keys who's values are of typeA

doStuff('b'); //this should fail

function doStuff(arg: keyof FilteredForTypeA): void {
  ...
}

Here's how I try to filter them out

type FilteredForTypeA = { [k in keyof TheRecord]: TheRecord[k] extends TypeA ? TheRecord[k] : never }

Solution

  • There's a few things going on here so I'll make an answer since it's not a direct duplicate of the relevant existing questions I found.

    When your type has an index signature it's hard to extract just the "known" literal keys of the object if they are subtypes of the index signature. That is, keyof {[k: string]: any, foo: any} is just string, and "foo" is completely subsumed in that. You can use a conditional type trick to extract just the known literal keys, as shown in this related question:

    type RemoveIndex<T> = {
        [K in keyof T as string extends K ? never : number extends K ? never : K]: T[K]
    };
    
    type KnownKeys<T> = keyof RemoveIndex<T>;
    

    On the other hand, you want only the keys whose values have a property matching a particular type. That is doable with a mapped-conditional-lookup, as shown in this related question:

    type KeysMatching<T, V> = { [K in keyof T]: T[K] extends V ? K : never }[keyof T];
    

    Put those together and you get:

    type KnownKeysMatching<T, V> = KeysMatching<Pick<T, KnownKeys<T>>, V>
    

    And you can verify that it works as I think you intend:

    function doStuff(arg: KnownKeysMatching<TheRecord, TypeA>): void {
    }
    
    doStuff('a'); // okay
    doStuff('b'); // error!
    doStuff('c'); // okay
    doStuff('d'); // error! 
    

    Note how arg cannot be 'b', as desired, but it also cannot be 'd' or any other "unknown" string, even though TheRecord has a string index signature. If you need some other behavior for 'd', that could be done, but it seems outside the scope of the question.

    Playground link to code