Jonathan Nielsen
Jonathan Nielsen

Reputation: 1502

Unexpected "never" in Typescript

So I'm trying to loop over properties of an object.

interface IProperties {
    foo?: {
        foo?: string[]
        bar?: string[]
    }
}

function init(properties: IProperties) {
    if (properties.foo) {
        for (const [key] of Object.entries(properties.foo)) {
            const prop = key as keyof typeof properties.foo

            if (properties.foo[prop].length) {
                properties.foo[prop] = properties.foo[prop].trim()
            }
        }
    }
}

I'm getting the error message "Property 'length' does not exist on type 'never'". Apparently the "prop" variable is expected to never have a value, but I can't understand why it does that.


EDIT:

Here's the TypeScript Playground

Upvotes: 1

Views: 529

Answers (1)

Shivam Singla
Shivam Singla

Reputation: 2201

Problem

First, we need to note the following points-

The keyof operator returns never if the type is "nullable" or can be undefined

type Keys = keyof ({a: string} | undefined)
// type of Keys is never

Next, inside the block of if, properties.foo is now not-undefined i.e. it's type is narrow-downed to exclude undefined. But it loses it's narrowness in the inner scope of the loop (https://github.com/Microsoft/TypeScript/issues/30576).

Ergo, while evaluation the type of keyof typeof properties.foo, properties.foo includes undefined. So, the type assertion resolves to never.

Solution

There are two possible solutions-

  1. Use NonNullable in the loop
const prop = key as keyof NonNullable<typeof properties.foo>

Playground

  1. Move the type resolution in the if block.
interface IProperties {
    foo?: {
        foo?: string[]
        bar?: string[]
    }
}

function init(properties: IProperties) {
    if (properties.foo) {
        type Keys = keyof typeof properties.foo
        for (const [key] of Object.entries(properties.foo)) {
            const prop = key as Keys

            if (properties.foo[prop]?.length) {
                // TODO: fix the below line, properties.foo[prop] is `Array`
                // `trim` does not exists on `Array`
                // properties.foo[prop] = properties.foo[prop].trim()
            }
        }
    }
}

Playground


In my opinion, the best practice is to create type on where other types lie, not in the function definition.

interface IProperties {
    foo?: {
        foo?: string[]
        bar?: string[]
    }
}

type IPropertiesFooKeys = keyof NonNullable<IProperties['foo']>

function init(properties: IProperties) {
    if (properties.foo) {
        for (const [key] of Object.entries(properties.foo)) {
            const prop = key as IPropertiesFooKeys

            if (properties.foo[prop]?.length) {
                // TODO: fix the below line, properties.foo[prop] is `Array`
                // `trim` does not exists on `Array`
                // properties.foo[prop] = properties.foo[prop].trim()
            }
        }
    }
}

Playground

Upvotes: 4

Related Questions