John C
John C

Reputation: 1166

Filter interface keys for a sub-list of keys based on value type

Problem

Given a TypeScript interface that is being treated as a map (relating unique key types to non-unique value types), can one extract the key types that map to a specified value type?

As a concrete example, starting with the WindowEventMap in lib.dom.d.ts...

interface WindowEventMap 
    extends GlobalEventHandlersEventMap, WindowEventHandlersEventMap {
    "abort": UIEvent;
    "afterprint": Event;
    "beforeprint": Event;
    "beforeunload": BeforeUnloadEvent;
    "blur": FocusEvent;
    // ...
}

There is a built-in operator keyof for getting all the keys:

type FullKeyList = keyof WindowEventMap;
// gives: FullKeyList = "'abort' | 'afterprint' | 'beforeprint' | ..and 93 more"

Is there some expression that might filter only the keys which map to value type KeyboardEvent?:

// Goal:
type SubKeyList = SomeExpression<WindowEventMap, KeyboardEvent>;
// would give: SubKeyList = "'keyup' | 'keypress' | 'keydown'"

Solution Attempt

TypeScript has the Conditional Type expression:

T extends U ? X : Y

So this is a reasonable try:

type Filter<K extends keyof M, V, M> = M[K] extends V ? K : never;

It filters individual types:

// Passes this test:
type FilteredExample1 = Filter<'keyup', KeyboardEvent, WindowEventMap>;
// gives FilteredExample1 = "'keyup'"

// Passes this test:
type FilteredExample2 = Filter<'blur', KeyboardEvent, WindowEventMap>;
// gives FilteredExample2 = "never"

Can it filter a union of types and return a new union? Starts off looking good:

// Passes this test:
type FilteredUnionExample1 = Filter<'keyup' | 'keydown', KeyboardEvent , WindowEventMap>;
// gives FilteredUnionExample1 = "'keyup' | 'keydown'"

But the union fails if one or more members of the union fails:

// Fails this test:
type FilteredUnionExample2 = Filter<'keyup' | 'keydown' | 'blur', KeyboardEvent, WindowEventMap>;
// gives FilteredUnionExample2 = "never" (and not sub-union "'keyup' | 'keydown'")

// And so, it also fails the end-goal usage:
type AllKeysUnion = keyof WindowEventMap;
type FilteredUnionExample3 = Filter<AllKeysUnion, KeyboardEvent , WindowEventMap>;
// gives FilteredUnionExample3 = "never" (not sub-union "'keyup' | 'keypress' | 'keydown'")

Is there a solution to this problem?

Upvotes: 4

Views: 1565

Answers (1)

jcalz
jcalz

Reputation: 328272

I usually call this KeysMatching:

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

type SubKeyList = KeysMatching<WindowEventMap, KeyboardEvent>
// type SubkeyList = "keydown" | "keypress" | "keyup"

Playground link to code

Upvotes: 5

Related Questions