Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

TypeScript: pick properties with a defined type

Tags:

typescript

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; }
like image 966
Andrey Godyaev Avatar asked Oct 05 '17 10:10

Andrey Godyaev


People also ask

How do you access the properties of objects in TypeScript?

To dynamically access an object's property: Use keyof typeof obj as the type of the dynamic key, e.g. type ObjectKey = keyof typeof obj; . Use bracket notation to access the object's property, e.g. obj[myVar] .

What is TypeScript pick?

In TypeScript, pick is used to take the certain objects of an already defined interface to create a new model.

What is ReturnType TypeScript?

The ReturnType in TypeScript is a utility type which is quite similar to the Parameters Type. It let's you take the return output of a function, and construct a type based off it.


2 Answers

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.
like image 130
iainvdw Avatar answered Oct 27 '22 01:10

iainvdw


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
like image 29
hlovdal Avatar answered Oct 27 '22 00:10

hlovdal