0

I need to have an iterator over SomeType type properties. Object(instance) is not OK cause it returns string[] not Array<keyof SomeType>. The following function solves the problem

export function keysof<T>(obj: Partial<Record<keyof T, any>>): Array<keyof T> {
    return Object.keys(obj) as Array<keyof T>;
}

The thing I really dislike here is that obj argument seem to be redundant, cause type should be enough to get it's properties. I'd like to be able to do something like the following

export function keysof<T>(): Array<keyof T> {
    return Object.keys(T) as Array<keyof T>;
}

But obviously that will throw 'T' only refers to a type, but is being used as a value here compilation error.

Is it possible to avoid passing SomeType instance to function?

Eugeny89
  • 3,797
  • 7
  • 51
  • 98
  • 1
    In order to call `Object.keys` you should have an object. Hence, you `have` to pass this object as an argument to `keysof` function. Generic type `T` is erased after compilation. It means that you are trying to do this: `Object.keys()`. What do you expect from this? – captain-yossarian from Ukraine Oct 29 '21 at 10:00
  • 1
    `seem to be redundant` - it is not true. `obj` argument is `required` – captain-yossarian from Ukraine Oct 29 '21 at 10:01
  • 1
    @captain-yossarian good point. When I say redundant I mean that we're using only type of `obj`, and hence we're actually passing type for two times. But, yes, you're right, `T` exists only before ts compilation, so looks like it's impossible to remove this "redundancy" – Eugeny89 Oct 29 '21 at 11:35
  • 1
    What does "we're actually passing type for two times" mean? Where are the two times? Do you mean that `declare function f(x: T): void;` mentions `T` twice? If so, then note that `x => x * 3` mentions `x` twice but you wouldn't call it "redundant" and certainly couldn't replace it with `x => * 3`. Or do you mean something else? – jcalz Oct 29 '21 at 13:09
  • @captain-yossarian please add your comment as an answer, so that I can accept it as correct – Eugeny89 Oct 31 '21 at 07:17

1 Answers1

2

In order to call Object.keys you should have an object.

Hence, you have to pass this object as an argument to keysof function, like this:

const keysof=<T,>(obj: T) => Object.keys(obj) as Array<keyof T>;

Generic type T is erased after compilation.

It means that if you want to remove obj argument and leave only T, like this:

const keysof = <T,>() => Object.keys() as Array<keyof T>;

you will end up with this:

const keysof = () => Object.keys();

Hence, you can get rid only explicit return type, like this:

export function keysof<T>(obj:T) {
    return Object.keys(obj) as Array<keyof T>;
}

I think you should be aware of Array<keyof T> drawback. Because it allows you to use duplicates in your array. Consider this exmaple:

const obj = { age: 42, name: 'John' }

const keys: Array<keyof typeof obj> = ['age', 'age', 'age'] // no compilation error

Is this what you want ? I don't think so. Because Object.keys can't produce duplicates in the result array.

There is another one approach. It also has own drawbacks :)

// credits goes to https://twitter.com/WrocTypeScript/status/1306296710407352321
type TupleUnion<U extends string, R extends any[] = []> = {
  [S in U]: Exclude<U, S> extends never ? [...R, S] : TupleUnion<Exclude<U, S>, [...R, S]>;
}[U];


const obj = { age: 42, name: 'John' }

const keys: TupleUnion<keyof typeof obj> = ['age', 'age', 'age'] // error

Above code causes a compilation error, just as you expect. But you can also boil some water on your PC/laptop since it CPU resurse consuming approach. Also it has own recursion limitations.

Here you can find step by step explanation of TupleUnion type utility

TL;TR

TupleUnion - produces a permutation of all possible Object.keys results