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; }
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] .
In TypeScript, pick is used to take the certain objects of an already defined interface to create a new model.
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.
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:
P in keyof T
stores all possible keys of T
in P
as
uses P
to access T[P]
and get its valueT[P]
matches Value | undefined
(undefined
to allow for optional properties).T[P]
matches Value | undefined
, we then set P
as property of the type and its corresponding value of T[P]
never
don't end up in the resulting type, explicitly removing any properties that don't match the type you want to pick.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.
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
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With