Reputation: 913
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
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:
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.Upvotes: 63
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.
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
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