skip to content

In TypeScript, how do you write a mapped type that keeps only the properties of `T` whose values are functions, and where does the conditional have to go?

level: middleimportance: should knowfreq 32%

answer

  1. filter by what the value is
  2. the decision belongs in the key position
  3. T[K] is in scope inside the clause
  4. optional properties carry undefined
  5. never removes, never-in-value keeps

basics

~20 s

Put the conditional in the as clause: { [K in keyof T as T[K] extends Fn ? K : never]: T[K] }. Keys whose value type fails the test map to never and disappear. A conditional in the value position would keep the key instead.

solid answer

~40 s

You filter in the key position, because that is the only place a decision can *remove* a property. Write `{ [K in keyof T as T[K] extends (...args: any[]) => any ? K : never]: T[K] }`: `K` is still the original key inside the clause, so `T[K]` is available to test the value type, and the keys that fail map to `never` and are not emitted. The same shape parameterised gives a reusable `PickByValue<T, V>`. Two things to watch. Putting the conditional in the value position instead yields `{ save: never }` — the key survives, which is not filtering. And under `strictNullChecks` an optional property's type includes `undefined`, so `T[K] extends string` is false for `label?: string`; wrap it as `NonNullable<T[K]> extends string` if you want optionals to match.

code

typescript · 11 lines
typescript
type PickByValue<T, V> = {
  [K in keyof T as NonNullable<T[K]> extends V ? K : never]: T[K];
};

type Row = { id: number; label: string; render(): void };

type Strings = PickByValue<Row, string>;
type Actions = PickByValue<Row, (...args: any[]) => any>;

const s: Strings = { label: "hi" };
const a: Actions = { render() {} };

go deeper

for a junior

Know that never produced in the as clause makes a property disappear, and that the same never written as the property's type does not. Reading such a type is enough at this stage.

for a middle

Be ready to write PickByValue from memory, explain that T[K] is available inside the clause because K is still the original key, and name the optional-property trap where undefined breaks the test.

for a senior

Show that you decide the predicate deliberately — assignability, not equality — and that you know when the filtered type is worth the indirection versus simply declaring the narrower shape.

for a principal

Own where such helpers live and how many exist: a handful of well-named filters is leverage, a private zoo of near-identical ones is a maintenance surface with poor error messages.

## Why the key position A mapped type emits one property per iterated key. Anything you write in the *value* position decides what type that property has; only the `as` clause decides what the property is *called* — and `never` there means it is called nothing, so it is not emitted. Filtering is therefore a key-position operation by construction: ```ts type PickByValue<T, V> = { [K in keyof T as T[K] extends V ? K : never]: T[K]; }; ``` The conditional runs once per key. `K` is bound to the original key throughout the mapped type, so `T[K]` inside the clause is the property's declared type. When the test passes, the clause returns `K` and the property is emitted unchanged; when it fails, the clause returns `never` and there is no key to emit. ## The classic wrong answer ```ts type Broken<T, V> = { [K in keyof T]: T[K] extends V ? T[K] : never; }; ``` This compiles and looks like it works, but it filters nothing. Every key survives; the rejected ones just get the type `never`. That is materially worse than not filtering: `keyof Broken<T, V>` still lists them, object literals must still not mention them, and any code that tries to read one gets a value it can never legally produce. If an interviewer asks you to "remove" keys and you write this, expect to be pushed on it. ## Choosing the predicate For "is it a function", the usual predicates are the top function type `Function`, or the more precise signature `(...args: any[]) => any`. Both are real and both work; the signature form is preferred by codebases whose lint rules discourage the bare `Function` type. Whatever you pick, remember `extends` here means *assignable to*, not "is exactly": ```ts type Row = { id: number; label: string; render(): void }; type Actions = PickByValue<Row, (...args: any[]) => any>; // { render(): void } type Strings = PickByValue<Row, string>; // { label: string } ``` ## The optional-property trap This is the detail that separates a memorised answer from an understood one. Under `strictNullChecks`, a property declared `label?: string` has the type `string | undefined` when you look it up with `T["label"]`. And `string | undefined extends string` is false — a union is only assignable to a target if *every* member is. So the obvious filter silently drops every optional property: ```ts type T = { a: string; b?: string; c: number }; type Only = { [K in keyof T as T[K] extends string ? K : never]: T[K] }; // keyof Only is "a" — b was filtered out too ``` If optionals should match, strip the `undefined` first with the built-in `NonNullable`: ```ts type Only2 = { [K in keyof T as NonNullable<T[K]> extends string ? K : never]: T[K]; }; ``` Decide deliberately which behaviour you want; both are defensible, but the difference should be a choice and not a surprise. ## Distribution, if the value type is a union `T[K] extends V ? K : never` is a conditional over `T[K]`, which is a naked type parameter position only when you write it that way. Where a property's type is itself a union — `string | number` — the whole union is tested against `V` as one type, so a property typed `string | number` does **not** match `V = string`. Again this is the ordinary assignability rule, not a special mapped-type rule, but it is the second most common surprise after optionals. ## Getting the key names instead of the sub-object Sometimes what you actually want is the union of matching *names*, not a filtered object type. Apply `keyof` to the filtered result: ```ts type ActionKeys = keyof PickByValue<Row, (...args: any[]) => any>; // "render" ``` That composes cleanly, which is one practical reason to build the filtered object type first even when the key union is the goal. ## The tier this lives on All of this is compile-time bookkeeping. `PickByValue` does not walk an object, does not call `typeof`, and emits no code. It describes which properties of a shape are functions so that other signatures can be written against that answer — for example a helper that only accepts method names. If you need the same distinction while the program runs, that is a separate runtime check you have to write, and the type is at best its contract.

  • How would you get just the union of matching key names rather than the filtered object type?
    Apply `keyof` to the filtered mapped type: `keyof PickByValue<Row, (...args: any[]) => any>` gives `"render"`. Building the object type first and taking `keyof` composes better than trying to build the key union directly, and it keeps one helper serving both needs.
  • Why does `T[K] extends string` come out false for a property declared `label?: string`?
    Under `strictNullChecks` the optional marker puts `undefined` into the property's type, so `T["label"]` is `string | undefined`. A union is assignable only if every member is, and `undefined` is not a `string`, so the test fails and the key is filtered out. Use `NonNullable<T[K]> extends string` if optionals should match.
  • An interviewer writes `{ [K in keyof T]: T[K] extends Fn ? T[K] : never }` and calls it a filter. What do you say?
    That it filters nothing. Every key is still emitted; the non-matching ones just have the type `never`, so they still appear in `keyof`, still block object literals, and still show up in error messages. Moving the conditional into the `as` clause is what actually removes them.

saying these in an interview costs you the question

  • Puts the conditional in the value position and calls it filtering
  • Expects optional properties to match a bare string test
  • Thinks extends here means exact type equality
  • Believes the filtered type checks values at runtime
  • Says a property typed string | number matches V = string

context