Marco
Marco

Reputation: 644

How to create an interface with optional values

I have an object for which at the start I don't know what keys are going to be in it, but it seems that typescript seems to not consider the possibility that a value for a key might not exist:

interface ById {
    [key:string]: number[]
}

const test: ById = {
    dummyValue: [1,2,3]
}

const doThing = (key: string) => {
    // regardless of if this key exists, it still assumes it's an array
    test[key].push(4)
}

doThing('test')

I figured I'd make an interface that makes it explicit that the value for this item might be undefined, so that it shows an error if I don't check if it exists, but for some reason it shows me an error, even though I check.

interface ByIdOptional {
    [key:string]: number[] | undefined
}

const test: ByIdOptional = {
    dummyValue: [1,2,3]
}

const doThing = (key: string) => {
    if (typeof test[key] !== 'undefined') {
        // Even though I'm testing for undefined, it's showing an error here
        test[key].push(4)
    }
}

doThing('test')

Is there a better way of achieving this, and to get type safety code that uses this object? I've added the code to a Typescript Playground as well (for testing)

Upvotes: 0

Views: 47

Answers (2)

jcalz
jcalz

Reputation: 327624

Your | undefined for the index signature value type is a good idea; right now index signature types are implausibly assumed to have defined values at each key. It's more correct but potentially annoying for the compiler to always expect a possibly-undefined value in an index signature. There's an open GitHub issue tracking this (microsoft/TypeScript#13778) but I wouldn't expect any change here soon.

The problem with your check is that the compiler doesn't really have a way to track the sort of narrowing implied by typeof test[key] !== 'undefined'. The variable key is only known to be of type string. The only possible narrowing the compiler could do would be to say "all string-valued keys of test have been verified not to be undefined". That's not a desirable narrowing, so the compiler basically does nothing. There's a GitHub issue file about this, (microsoft/TypeScript#31445), which was closed as a design limitation. To address it the language would have to keep track of which individual variables were used as keys in type guards, which would be a lot of extra work for the occasional help it would provide.

The easiest workaround here is just to copy the value in question to a new variable, which can be checked via control flow analysis without worrying about re-doing property accesses:

const doThing = (key: string) => {
    const testKey = test[key];
    if (typeof testKey !== 'undefined') {
        testKey.push(4)
    }
}

Okay, hope that helps; good luck!

Playground link to code

Upvotes: 2

T.J. Crowder
T.J. Crowder

Reputation: 1073968

Assign test[key] to a local. TypeScript will then refine the type of the local based on the guard:

interface ByIdOptional {
    [key:string]: number[] | undefined
}

const test: ByIdOptional = {
    dummyValue: [1,2,3]
}

const doThing = (key: string) => {
    const array = test[key]
    if (typeof array !== 'undefined') {
        array.push(4)  // <=================== No error here
    }
}

doThing('test')

On the playground

Upvotes: 1

Related Questions