Preserving type when using Object.keys()
Asked Answered
F

3

12

I have an object with typed keys, let's call them Statuses (StatusesType). I need to iterate over the object and pass keys to some method expecting parameter of the same type StatusesType, let it be statusPrinter()

type StatusesType = 'PENDING' | 'APPROVED' | 'REJECTED';
type SomeMap = {
    [key in StatusesType]?: number
}

const STATUSES: SomeMap = {
    PENDING: 5,
    REJECTED: 2,
};

function statusPrinter(val: StatusesType) {
    console.log('- ', val);
}

Object.keys(STATUSES).forEach(status => {
    statusPrinter(status);
});

But when I call statusPrinter(status); TypeScript returns this error

error TS2345: Argument of type 'string' is not assignable to parameter of type 'StatusesType'.

How can I pass this key preserving type?

I know that I can force TS with this statusPrinter(<StatusesType>status); but I think it is the last thing I should do and I would prefer native solution.

Update: If it is not possible to iterate over object keys with Object.keys() preserving type - what options do I have? Is there a way to iterate over keys preserving types at all, and if so - which way is the best? I am not fixing on Object.keys() but I would like to keep original object structure.

Thanks!

Fakieh answered 15/9, 2017 at 6:42 Comment(4)
How can I pass this key preserving type? I don't think you can. keys will always be string. You can try to use for (let key:StatusesType in STATUSES ) and try, but I doubt it would work.Judaize
Why Object.keys isn't typed: github.com/Microsoft/TypeScript/pull/…. How about .forEach((status: StatusesType) => {?Invective
I've updated the question.Fakieh
@jonrsharpe, It actually works, and could be a... well if not a solution still good kin of workaround. I would prefer not to specify type, but if it is an only option - still good. Thanks.Fakieh
G
7

Short and typesafe solution using the built-in ES2015 Map class:

type StatusesType = 'PENDING' | 'APPROVED' | 'REJECTED';

const STATUSES = new Map<StatusesType, number>([
    ['PENDING', 5],
    ['REJECTED', 2],
]);

function statusPrinter(val: StatusesType) {
    console.log('- ', val);
}

STATUSES.forEach((_, status) => statusPrinter(status));
Garage answered 15/9, 2017 at 20:53 Comment(2)
Oh, this is good. It requires little more typing than I expected but it is type-safe.Fakieh
Probably the best suggestion, Avoid using Object.keys as it requires casting (casting is dangerous) Using a custom iterator function as suggested here is okay, a little ugly to my taste, but also does the workEffluence
J
4

Object.keys will return an array of keys and keys are of type string.

So signature of Object.keys would be key(object: {}): Array<string>. So when you loop over keys, status is of type string and not StatusesType.

You can cast the type though as statusPrinter(status as StatusesType)

Reference Link:

Judaize answered 15/9, 2017 at 6:47 Comment(1)
As I mentioned in my question - I know that I can case but I do not want to cast, I am looking for native way of passing type...Fakieh
H
2

You can do that with iterator like this:

function iterator<M, K extends keyof M>(map: M, cb: (key: keyof M, value: M[K]) => void) {
  Object.keys(map).forEach((key: K) => cb(key, map[key]))
}

iterator(STATUSES, status => {
    statusPrinter(status);
});
Haustellum answered 17/9, 2017 at 18:57 Comment(3)
Also good: Does not require using Map but little less convenient because in case if I need also value TypeScript would recognize STATUSES[status] in loop as potential undefined, because of the way object type is defined [key in StatusesType]?.Fakieh
iterator function seems to have type errors: codesandbox.io/s/relaxed-leakey-93zb3?file=/src/index.tsPurificator
I get multiple type errors in the iterator function. type M does not work with the signature of Object.keys and it can't type key in the forEach lambda. See the codesandbox link of @QuangVanCurculio

© 2022 - 2024 — McMap. All rights reserved.