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?
answer
- filter by what the value is
- the decision belongs in the key position
- T[K] is in scope inside the clause
- optional properties carry undefined
- never removes, never-in-value keeps
basics
~20 sPut 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 sYou 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 linestype 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
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.
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.
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.
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