2

I was wondering if it was possible do something like this:

type FooBar = {
    foo: number,
    bar: number
}

const obj = {
    foo: 3,
    bar: 2
}

const keys = KeysOf<FooBar>() // keys = ["foo", "bar"]
function isFooBar(obj: any): boolean {
    for(key of keys) {
        if (!(key in obj)) {
            return false 
        }
    }
}

I know that types in TypeScript are a development concept, but it could be possible to implement this by making the TypeScript compiler extract those keys.

kelsny
  • 23,009
  • 3
  • 19
  • 48
  • 1
    As written what you want is impossible. That will compile to `const keys = KeysOf();` and so you'd be calling a zero-arg function named `KeysOf()` and hoping it will magically know what type's keys you want. If you want an array at runtime you will have to make it yourself, like `const keys: Array = ["foo", "bar"]`. – jcalz Mar 11 '23 at 13:34
  • Probably a dupe of [this](https://stackoverflow.com/questions/43909566/get-keys-of-a-typescript-interface-as-array-of-strings) – kelsny Mar 11 '23 at 15:39

1 Answers1

2

You can't do it that way around, because TypeScript type information isn't available at runtime. You have to have a runtime array, ["foo", "bar"], in order to implement your type predicate. But that would seem to mean having the names in two places, which is a maintenance concern.

When I'm in this situation, I usually turn it on its head and create the runtime array, then define both the type and the type predicate in terms of the array, like this:

const fooBarKeys = ["foo", "bar"] as const;

type TypeHelper<Keys extends PropertyKey> = {
    [Key in Keys]: number;
};

type FooBar = TypeHelper<typeof fooBarKeys[number]>;
//   ^? type FooBar = { foo: number; bar: number; }

function isFooBar(obj: any): obj is FooBar {
    for (const key of fooBarKeys) {
        if (typeof obj[key] !== "number") {
            return false;
        }
    }
    return true;
}

Example use of the type predicate:

console.log(isFooBar({foo: 42, bar: 67}));  // true
console.log(isFooBar({foo: 42}));           // false

Playground link

You can also do it by using a "model object," which is just a different approach to the same concept of building the type information from data structures that are also available at runtime:

const fooBarModel = {
    foo: 42,
    bar: 67,
};

type FooBar = typeof fooBarModel;
//   ^? type FooBar = { foo: number; bar: number; }

const fooBarKeys = Object.keys(fooBarModel) as Array<keyof FooBar>;

function isFooBar(obj: any): obj is FooBar {
    for (const key of fooBarKeys) {
        if (typeof obj[key] !== typeof fooBarModel[key]) { // This is imperfect, but better than nothing
            return false;
        }
    }
    return true;
}

console.log(isFooBar({foo: 42, bar: 67}));  // true
console.log(isFooBar({foo: 42}));           // false

Playground link

T.J. Crowder
  • 1,031,962
  • 187
  • 1,923
  • 1,875