3

I want to traverse the properties of an object and have Typescript correcly infer the type of the key, but I can't seem to achieve, and I have to cast it.

Here's an example

type Cycle = 'year' | 'month' | 'week';

type Price = {
    [key in Cycle]?: number
}

const price1: Price = { year: 20, month: 40 }

for (const key in price1) {
    const cycle = key as keyof typeof price1 // wanna get rid of this cast
    console.log({ key, cycle }) // --> key: string, cycle: Cycle
}

Here the typescript playground if anybody wants to give it a try

I'd like to know whay Typescript can't infer it, if there's any "risk" in doing that cast, and if there's a better, more elengant, safer or correct way to do it.


Edit: taking into account the answer I got, I came to this version using a type guard function with a type predicate:

type Cycle = 'year' | 'month' | 'week';

type Price = {
    [key in Cycle]?: number
}

const price1: Price = { year: 20, month: 40 }

const isCycle = (cycle: any): cycle is Cycle => typeof(cycle) === 'string' && ['year', 'month', 'week'].includes(cycle)

for (const key in price1) {
    if (isCycle(key)) {
        console.log({ key }) // --> inside here key is a Cycle
    }
    console.log({ key })  // --> and here key is a string
}

And this is the typescript playground

However I have to keep in sync the type Cycle = 'year' | 'month' | 'week' with the isCycle function, and it gets a bit verbose.

Is there a simpler way to do this in a safe way?

1 Answer 1

3

This isn't possible in a safe way, and you really shouldn't do that cast (type assertion is the correct term).

The reason the compiler won't ever make this inference is because it can't actually guarantee that key will only be one of either year, month or week.

This is because of structural typing: in TypeScript, given a type Price, it is perfectly valid to assign an object that has extra properties to a variable typed Price, like this:

type Cycle = 'year' | 'month' | 'week';

type Price = {
    [key in Cycle]?: number
}

const otherPrice = {
    year: 20,
    month: 40,
    anotherWeirdKey: 'fish'
}

let price1: Price = { year: 20, month: 40 }
price1 = otherPrice

This doesn't produce an error because otherPrice has all the correct properties. Since types don't exist at runtime, that means that regardless of the type of price, your loop is going to produce anotherWeirdKey as one of its values.

Edit

You've edited your question to ask how to create a type guard that makes your code safe without having to keep updating the type guard.

The solution to this is to start with your options in an array literal and extract a type from that, rather than the other way round. This way you can just re-use the array literal in your type guard. Like this

const cycle = [
    'year',
    'month',
    'week'
] as const

type Cycle = typeof cycle[number];

const isCycle = (cycle: string): cycle is Cycle => cycle.includes(cycle)
Sign up to request clarification or add additional context in comments.

1 Comment

very clear explnation, so the safe way would be to validate that key is effectively a Cycle, please have a look at the updated question, thanks.

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.