How to define a Type from a Class/Interface that includes only class members of specified types

Issue

Let me explain my idea in this example. Lets say I have this class:

class Class1 {
  f1!: string;
  f2!: string;
  f3!: number;
  f4!: Date;
}

If I need a type with the members of types string from that class.
I can do for example

type Class1Stings = Omit<Class1, 'f3' | 'f4'>;

But I need to know exact properties names for this. I want to have something like this:

type Class1Stings = KeepOnlyTypes<Class1, string>;
// or 
type Class1Stings = RemoveTypes<Class1, number | Date>;

Is this possible?

============= Edit:

my playground link

Solution

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:

  1. 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;
    }
    
  2. 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.)

The answer is: It is working, but the assignDates function isn’t typesafe, because although Class1 may have only Date properties (specifically), subclasses of it can refine those into a 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.)

Answered By – T.J. Crowder

Answer Checked By – Candace Johnson (AngularFixing Volunteer)

Leave a Reply

Your email address will not be published.