Andrey Godyaev
Andrey Godyaev

Reputation: 913

TypeScript: pick properties with a defined type

Is it possible to construct a TypeScript type that will pick only those properties that are typeof X?

interface IA {
    a: string;
    b: number;
}

interface IB extends IA {
    c: number | string;
}

type IAStrings = PickByType<IA, string>;
// IAStrings = { a: string; }
type IBStrings = PickByType<IB, string>;
// IBStrings = { a: string; }
type IBStringsAndNumbers = PickByType<IB, string | number>;
// IBStringsAndNumbers = { a: string; b: number; c: number | string; }

Upvotes: 36

Views: 25776

Answers (3)

iainvdw
iainvdw

Reputation: 802

Using Typescript 4.1, this can be made even shorter, while also allowing to pick optional properties, which the other answers don't allow:

type PickByType<T, Value> = {
  [P in keyof T as T[P] extends Value | undefined ? P : never]: T[P]
}

As an explanation what happens here, because this might come across as black magic:

  1. P in keyof T stores all possible keys of T in P
  2. The as uses P to access T[P] and get its value
  3. We then go into the conditional where it checks if T[P] matches Value | undefined (undefined to allow for optional properties).
  4. If the value of T[P] matches Value | undefined, we then set P as property of the type and its corresponding value of T[P]
  5. Type properties set to never don't end up in the resulting type, explicitly removing any properties that don't match the type you want to pick.

Upvotes: 63

hlovdal
hlovdal

Reputation: 28208

Yes, this is possible. I arrived at this question searching for the answer as well, but I eventually figured it out.

TL;DR

/**
 * Returns an interface stripped of all keys that don't resolve to U, defaulting 
 * to a non-strict comparison of T[key] extends U. Setting B to true performs
 * a strict type comparison of T[key] extends U & U extends T[key]
 */
type KeysOfType<T, U, B = false> = {
  [P in keyof T]: B extends true 
    ? T[P] extends U 
      ? (U extends T[P] 
        ? P 
        : never)
      : never
    : T[P] extends U 
      ? P 
      : never;
}[keyof T];

type PickByType<T, U, B = false> = Pick<T, KeysOfType<T, U, B>>;

Longer explanitary version

class c1 {
  a: number;
  b: string;
  c: Date;
  d?: Date;
};

type t1 = keyof c1; // 'a' | 'b' | 'c' | 'd'
type t2 = Pick<c1, t1>; // { a: number; b: string; c: Date; d?: Date; }

type KeysOfType0<T, U> = {
  [P in keyof T]: T[P] extends U ? P : never;
};
type t3 = KeysOfType0<c1, Date>; // { a: never; b: never; c: "c"; d?: "d"; }

// Based on https://github.com/microsoft/TypeScript/issues/16350#issuecomment-397374468
type KeysOfType<T, U> = {
  [P in keyof T]: T[P] extends U ? P : never;
}[keyof T];

type t4 = KeysOfType<c1, Date>; // "c" | "d"

type t5 = Pick<c1, t4>; // { c: Date; d?: Date; }

type PickByType<T, U> = Pick<T, KeysOfType<T, U>>;

type t6 = PickByType<c1, Date>; // { c: Date; d?: Date; }

So with that PickByType gives exactly the result you have in the comments.


Edit

If you need a strict type utility, you need to verify that the extends goes both ways. Below is an example of one case where the original KeysOfType utility might return unexpected results, and two solutions.

Try it out on the typescript playground.

type KeysOfType<T, U> = {
  [P in keyof T]: T[P] extends U ? P : never;
}[keyof T];

type PickByType<T, U> = Pick<T, KeysOfType<T, U>>;

type KeysOfTypeStrict<T, U> = {
    [P in keyof T]: T[P] extends U ? (U extends T[P] ? P : never) : never;
}[keyof T];
type PickByTypeStrict<T, U> = Pick<T, KeysOfTypeStrict<T, U>>;

/**
 * Returns an interface stripped of all keys that don't resolve to U, defaulting 
 * to a non-strict comparison of T[key] extends U. Setting B to true performs
 * a strict type comparison of T[key] extends U & U extends T[key]
 */
type KeysOfTypeBest<T, U, B = false> = {
  [P in keyof T]: B extends true 
    ? T[P] extends U 
      ? (U extends T[P] 
        ? P 
        : never)
      : never
    : T[P] extends U 
      ? P 
      : never;
}[keyof T];

type PickByTypeBest<T, U, B = false> = Pick<T, KeysOfTypeBest<T, U, B>>;


interface thing {
  foo: () => string;
  bar:  (resourceName: string) => string;
  test: string;
}

type origBar = PickByType<thing, thing['bar']>;
let origBar: Partial<origBar> = {};
origBar.bar; // success: true positive
origBar.foo; // success: false positive, I wasn't expecting this property to be allowed.
origBar.test // error: true negative

type origFoo = PickByType<thing, thing['foo']>;
let origFoo: Partial<origFoo> = {};
origFoo.bar; // error: true negative
origFoo.foo; // success: true positive
origFoo.test // error: true negative

type strictBar = PickByTypeStrict<thing, thing['bar']>;
let strictBar: Partial<strictBar> = {};
strictBar.bar; // success: true positive
strictBar.foo; // error: true negative
strictBar.test // error: true negative

type strictFoo = PickByTypeStrict<thing, thing['foo']>;
let strictFoo: Partial<strictFoo> = {};
strictFoo.bar; // error: true negative
strictFoo.foo; // sucess: true positive
strictFoo.test // error: true negative

type bestBarNonStrict = PickByTypeBest<thing, thing['bar']>;
let bestBarNonStrict: Partial<bestBarNonStrict> = {};
bestBarNonStrict.bar; // success: true positive
bestBarNonStrict.foo; // success: true positive, I do want to keep properties with values similar to bar
bestBarNonStrict.test // error: true negative

type bestBarStrict = PickByTypeBest<thing, thing['bar'], true>;
let bestBarStrict: Partial<bestBarStrict> = {};
bestBarStrict.bar; // success: true negative
bestBarStrict.foo; // error: true negative, I do NOT want to keep properties with values similar to bar
bestBarStrict.test // error: true negative

Upvotes: 31

Fenton
Fenton

Reputation: 251012

You may be able to do this in the future using enhancements to Mapped Types.

Until then, you could decompose your interfaces, although I only recommend this if it makes sense:

interface IA {
    a: string;
}

interface IA2 extends IA {
    b: number;
}

interface IB extends IA2 {
    c: number | string;
}

This gives you the IA type your were trying to calculate.

If this doesn't make sense, it will be a case on manually creating your IAStrings interface, which contains just the properties you want it to have.

Upvotes: 0

Related Questions