0

Suppose a type...

type Thing = {
    name: string;
    // other properties
};

...and an object which looks like this...

const obj = {
    name: 'blah',
    // other properties from Thing
    someProp: true,
    otherProp: 123,
    yetAnotherProp: 'yay',
};

...I want to be able to narrow the properties in obj to only those not in Thing, so the result would be:

const objWithoutThing = {
    // no properties from `Thing`
    someProp: true,
    otherProp: 123,
    yetAnotherProp: 'yay',
};

E.g.:

const objWithoutThing = Object
    .keys(obj)
    .filter(key => key not in keyof Thing) // obv not legit, what to put here?
    .reduce((o, key) => ({ ...o, [key]: obj[key] }), {});

// now `objWithoutThing` contains all properties of `obj` except those defined on `Thing`
Josh M.
  • 26,437
  • 24
  • 119
  • 200

2 Answers2

1

If you are using TypeScript v3.5 or above, you can use Omit

type Obj = {
    name: string;
    someProp: boolean;
    otherProp: number;
    yetAnotherProp: string;
}

type Thing = {
    name: string;
}

type ObjWithoutThing = Omit<Obj, keyof Thing>;

const objWithoutThing: ObjWithoutThing = {
    // no properties from `Thing`
    someProp: true,
    otherProp: 123,
    yetAnotherProp: 'yay',
};
Owl
  • 6,337
  • 3
  • 16
  • 30
  • Sorry, but the additional properties could be literally anything, so I can't define `ObjWithoutThing` at all, its definition is "any key, except those in `Thing`". – Josh M. Jan 18 '21 at 02:37
1

You can't do this automatically. TypeScript's static type system, including the definition of the Thing type alias, gets erased when the code is transpiled to JavaScript. So there will be nothing left at runtime to act upon when you try to omit the keys of Thing from obj.


If you want to omit the keys of Thing at runtime, you will need to explicitly create a list of such keys as a value that exists at runtime. With some effort you can get the compiler to at least check that you've done so correctly. For example:

const thingKeys = ["name"] as const;

type ThingKeysCheck<
    T extends typeof thingKeys[number] = keyof Thing,
    // --------------------------------> ~~~~~~~~~~~
    // error here if thingKeys has missing keys
    U extends keyof Thing = typeof thingKeys[number]
    // -------------------> ~~~~~~~~~~~~~~~~~~~~~~~~
    // error here if thingKeys has extra keys
    > = void;

That const assertion lets the compiler realize that thingKeys contains the string literal "name" (otherwise it would just see thingKeys as a string[]).

And ThingKeysCheck is just there as a check to see if the string literal elements of thingKeys are exactly keyof Thing. If there are no errors in that line, then thingKeys and Thing are consistent with each other. Otherwise there will be an error mentioning whether keys are missing or extra or both.


Once you have thingKeys, you can use it in your filter. The following withoutThing() function has a generic call signature saying that if you pass in obj of a type T assignable to Thing is passed in, then you will get a value of type Omit<T, keyof Thing> using the Omit<T, K> utility type:

function withoutThing<T extends Thing>(obj: T): Omit<T, keyof Thing> {
    return Object
        .keys(obj)
        .filter(k => !(thingKeys as readonly string[]).includes(k))
        .reduce<any>((o, k) => ({ ...o, [k]: obj[k as keyof Thing] }), {});
}

I did a lot of type assertions inside the implementation of that function to quiet the compiler complaints; it's not very good at verifying whether you are actually creating a valid object of type Omit<T, keyof Thing>. Anyway, it's basically the same implementation as you have written.


Let's see if it works:

const objWithoutThing = withoutThing(obj);
console.log(objWithoutThing.someProp); // true
console.log(objWithoutThing.otherProp.toFixed(2)); // 123.00
console.log(objWithoutThing.yetAnotherProp.toUpperCase()); // "YAY"
console.log(objWithoutThing.name); // undefined
// -----------------------> ~~~~
// Property 'name' does not exist

Looks good to me.


Playground link to code

jcalz
  • 264,269
  • 27
  • 359
  • 360
  • Yep, that makes sense and works. I was hoping TS had some magical `copykeys`, or something to extract the keys to a `string[]` at runtime. I'll go with this, though. Thanks. – Josh M. Jan 18 '21 at 02:43
  • If you're up for another ... https://stackoverflow.com/questions/65767666/define-typescript-type-with-properties-and-then-any-other-key-but-of-recursive – Josh M. Jan 18 '21 at 02:57