C B
C B

Reputation: 417

Object is possibly `undefined` even though it can't be

Reading through other answers it may be how Typescript statically analyses the code, if that is the case if someone is able to explain further that'd be great.

Here is my code, I can't see how it could be undefined, yes you may call foo({}) or foo({ algorithm: undefined }) but it'd be then merged with default and the resulting object would always have algorithm

type Options = {
    algorithm?: string;
    identifier?: string;
}

const defaults: Options = {
    algorithm: 'sha256',
    identifier: 'Fizz',
}

function foo(options: Options = defaults) {
    options = { ...defaults, ...options };

    if (!(options.algorithm in someOtherObj)) { // Object is possibly 'undefined'.

    }

    if (Object.prototype.hasOwnProperty.call(someOtherObj, options.algorithm)) {
      /**
        Argument of type 'string | undefined' is not assignable to parameter of type 'PropertyKey'.
          Type 'undefined' is not assignable to type 'PropertyKey'.
      **/
    }

    if (someOtherObj[options.algorithm]) { // Type 'undefined' cannot be used as an index type.
                
    }
}

foo();

Playground link: https://www.typescriptlang.org/play?ssl=27&ssc=7&pln=1&pc=1#code/C4TwDgpgBA8mwEsD2A7AzlAvFA3gKCkKgEMAbAcyQCcFgALAWwH4AuKNYGlcgbgKIQATCCkQAzBBCqt2nBNz4BfPHgDGqDlGFjiAV1LA0bOIg1Zc-QmUo16DNgHI0dYgCYArADYHAGktQhEXFJKkcAMQQAL0jfPGU8MV0UVVMUKDEkJAAKJHhkdGM8s2xtPQM0AEoLIihc1IxsHCgAOlbS-UMfFta6-IxFPn8EMSgsgEIcovRm62paRgC03o0Kqvx-eJrh0ZgAIwArCBTmlzQYAHcUAAUqXKlQZtUyUkn6ruXp2dtGVYsNlS2I1efQA2h80DMKHM7ABdNb+GqIoj-eIZbIVHhAA

Upvotes: 1

Views: 544

Answers (2)

Roustalski
Roustalski

Reputation: 156

The options parameter in foo is typed as Options, which is defined as having optional properties, regardless of what you do to the assignment of the variable.

Finally, the implementation of the foo function assumes that a function lives on the options object itself, which isn't typed anywhere. To make it type safe and double the options variable as the host for the actual functions, you could do something like the following:

type Algorithms = 'sha256';

type Options = {
    algorithm?: Algorithms;
    identifier?: string;
}

type AlgorithmFns = {
    [key: string]: string | Function;
}

const defaults: Required<Options> & AlgorithmFns = {
    algorithm: 'sha256',
    identifier: 'Fizz',
    sha256: () => {}
}

function foo(options?: Options) {
    const opts: Required<Options> & AlgorithmFns = { ...defaults, ...options };

    if (!(opts.algorithm in opts)) {

    }

    if (Object.hasOwnProperty.call(opts, opts.algorithm)) {

    }

    if (typeof opts[opts.algorithm] === 'function') {
                
    }
}

foo();

On the playground

Upvotes: 1

Alex Wayne
Alex Wayne

Reputation: 187272

The problem is the type of options is declared as Options. If you assign a new value to that type, it's still of type Options, and Options can have undefined properties.

Same goes for defaults. It's of type Options, so the fact that you've assigned strings for its properties doesn't matter. It's treated as type Options because you've told it to be that. type.

In short, by declaring type of a variable or constant, you override what typescript might infer about it.


I would model this differently.

Make the Options type require all properties.

type Options = {
    algorithm: string;
    identifier: string;
}

Now defaults can use that type, and it will even enforce you have a value for each property.

const defaults: Options = {
    algorithm: 'sha256',
    identifier: 'Fizz',
}

Now your function can say it only needs a Partial<Options> meaning that all properties are considered optional. Then merge your defaults into a new variable. Now typescript can see that no value could be undefined because one of the types you are merging it with cannot have undefined properties.

function foo(options: Partial<Options> = {}) {
    const optionsNotNull = { ...defaults, ...options };
    //...
}

Or more conveniently with destructuring:

function foo(options: Partial<Options> = {}) {
    const { algorithm, identifier } = { ...defaults, ...options };
    //...
}

Playground


Lastly, if (options[options.algorithm]) isn't going to work because Options is not indexable by string. Or to put another way, sha256 is not a property of options. So typescript won't let you do that. None of the above will change that.

Upvotes: 2

Related Questions