6

When I write some code, I have some problems like that:


function getObjectKeys<T extends object>(object: T) {
    return Object.keys(object) as (keyof T)[]
}

const props = {
    propA: 100,
    propB: 'text'
}

const store = { ...props }

getObjectKeys(props).forEach((key) => {
    store[key] = props[key]
})

reported some errors:

const store: {
    propA: number;
    propB: string;
}
Type 'string | number' is not assignable to type 'never'.
  Type 'string' is not assignable to type 'never'.

when I write like this:


getObjectKeys(props).forEach((key) => {
    if (key === 'propA') {
        store[key] = props[key]
    } else if (key === 'propB'){
        store[key] = props[key]
    } else {
        store[key] = props[key]
    }
})

It can work but not so good. how to solve them?

2 Answers 2

7

The underlying issue is a type safety improvement implemented by microsoft/TypeScript#30769 when doing assignments to unions of object properties. When faced with code like

type PropKeys = keyof typeof props; // "propA" | "propB"
getObjectKeys(props).forEach((key: PropKeys) => {    
    store[key] = ... // what is allowable here
});

the compiler sees that key is of the union type "propA" | "propB". It doesn't know whether key is "propA" or "propB", so to be safe, it only wants to allow assignments which would work no matter which it turns out to be. That means the intersection of the property types. Since these types are number and string, the intersection is number & string, which reduces to the impossible never type because no values are of both types. And so the compiler cannot allow any assignment to store[key].

The fix in this case is to replace values of union types with values of generic types that are constrained to the union. When the key or object is generic, the safety check from microsoft/TypeScript#30769 is not applied. This is potentially unsafe, but it is allowed, as mentioned in this comment. It looks like this:

getObjectKeys(props).forEach(<K extends PropKeys>(key: K) => {
  store[key] = props[key]; // okay
})

That compiles with no error, and is as close to type safe as you can get. If you change the assignment to something definitely unsafe, like copying props.propA into store[key], you will get a warning again:

getObjectKeys(props).forEach(<K extends PropKeys>(key: K) => {  
  store[key] = props.propA; // error!
})

Playground link to code

Sign up to request clarification or add additional context in comments.

1 Comment

thx so so much! u totally answered my confusion. At first I also tried generics, but the syntax failed. now i get the right situation : )
0

You should avoid using object in TS. Its not recommend or ideal solution instead use Record<..., ...>.

Target object store must to be Record<string, any> to get your code working since TS don't know which properties will be added ahead of time.

function getObjectKeys<T extends Record<string, any>>(object: T) {
    return Object.keys(object) as (keyof T)[]
}

const props = {
    propA: 100,
    propB: 'text'
}

const store: Record<string, any> = {}

getObjectKeys(props).forEach((key) => {
    store[key] = props[key]
})

playground link

4 Comments

"You should avoid using extends object in TS. Its not recommend or ideal solution instead use Record<..., ...>" This isn't true. extends object is fine; where do you see that it's not recommended? Not recommended by whom? In any case, changing it to extends Record<string, any> doesn't do anything to fix the problem.
@jcalz see this and example it was introduced to represents the non-primitive types so you should not use it to for custom types.
I remember reading something that was explaining briefly why we should avoid but I can't find it as of now.
There was an ESLint rule that discouraged using object, but ESLint is a third party tool... and nobody from the TS team ever said this. It's hard to consume a value of type object, but that doesn't apply to T extends object, which is 100% useful and recommended. If you can find an authoritative source that can explain why T extends object should be removed, you should include it in your answer. If you leave it unattributed then it reads like opinion, and since it is factually incorrect, I will downvote if its left there.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.