It's a bit tricky, but you can do it by first finding the keys for those properties (using a trick I picked up here), then using Pick as you indicated.
type PickStringProps<Source extends object> = Pick<Source, {
[Key in keyof Source]: Source[Key] extends string ? Key : never;
}[keyof Source]>;
// Usage:
class Class1 {
f1!: string;
f2!: string;
f3!: number;
f4!: Date;
}
type Class1Strings = PickStringProps<Class1>;
// ^?
Playground link (Explanation below.)
Alternatively, here's a reusable generic version:
// A type that picks out the keys for properties from `Source` that are
// assignable to `PickType`
type KeysByType<Source extends object, PickType> = {
[Key in keyof Source]: Source[Key] extends PickType ? Key : never;
}[keyof Source];
// A type that picks the properties from `Source` that are assignable to `Picktype`
type PickByType<Source extends object, PickType> =
Pick<Source, KeysByType<Source, PickType>>;
(We could combine the two above, but this is a bit clearer.)
Usage:
class Class1 {
f1!: string;
f2!: string;
f3!: number;
f4!: Date;
}
type Class1Strings = PickByType<Class1, string>;
// ^?```
Playground link
Copying from my answer here, here's how KeysByType works — there are two parts to it:
The {/*...*/} part maps each property of Source into a new mapped type where the type of the property is the key itself or never. Suppose we had:
type Example = {
a: string;
b: string;
c: number;
};
Just the first part of KeysByType<Example, string> creates this anonymous type:
{
a: "a";
b: "b";
c: never;
}
Then the [keyof Source] part at the end creates a union of the types of those properties, which in theory would be "a" | "b" | never, but never is always dropped from union types, so we end up with "a" | "b" — the keys of Example for properties with array types.
Then to create PickByType, we use Pick as you indicated.
In a comment you've asked why this doesn't seem to be working here:
function assignDates<T extends Class1>(
obj: T,
source: Record<KeysByType<T, Date>, string> | undefined,
keys: KeysByType<T, Date>[]
): void {
if (source) {
for (const key of keys) {
if (source[key] !== undefined) {
obj[key] = new Date(source[key]);
// ^^−−− Type 'Date' is not assignable to type 'T[KeysByType<T, Date>]'.(2322)
}
}
}
}
(Note that I've updated that a bit from your original: I changed the name json to source since its value isn't JSON [that would be a string], and fixed the type so you don't need the as any on source[key]: its values are strings, not Dates, and from the code it appears to be optional.)
It took me a while to understand why that didn't work too. :-) The answer is: It is working, but the assignDates function isn't typesafe, because although Class1's Date properties are specifically Date, subclasses of it can refine those to a Date subtype:
class MyDate extends Date {
myNiftyMethod() { /*...*/ }
}
class Class2 extends Class1 {
f4: MyDate;
// ...
}
Class2 fits the type constraint extends Class1 so you could pass it into assignDates, but f4 isn't just a Date anymore, it's a MyDate, and so you can't assign a Date to it.
Of course, a non-generic version of assignDate has that same Class2 problem, but doesn't raise an error on the obj[key] assignment. I expect this is some kind of limitation of TypeScript.
Given that the non-generic version doesn't have an error, you might just tell TypeScript to ignore the error, but you might want to post a question (since we're pretty far afield of the original question you asked here) asking whether there's a way to write a generic version in a typesafe way. (If you do, please link to it in the comments below; I'll be interested to see the answers.)