"Object is possibly undefined" when that is not possible

1.1k Views Asked by At

I'm defining a CDK construct that receives a props object and passes it to a PythonFunction's environment. Since the environment argument requires non-null string values (and some of the props are nullable or of non-string types), I have the following:

export interface MyConstructProps {
   key1: string,
   key2: string,
   key3?: string
   key4?: SomeOtherType
   [key: string]: string | SomeOtherType | undefined
 }

export class MyConstruct extends cdk.Construct {
  constructor(scope: Construct, id: string, props: MyConstructProps) {
    ...

    // within PythonFunction definition
    environment: Object.fromEntries(
      Object.entries(props)
        .filter(([k, v]) => v !== undefined)
        .map(([k, v]) => [k, v.toString()])
    ),
    ...
}

However, this gives an error - the v in v.toString() is red-underlined with the message TS2532: Object is possibly 'undefined'..

I've found that reversing the order of the map and filter compiles and works as expected:

Object.entries(props)
  .map(([k, v]) => {
    if (v === undefined) {
      return [k, v]
    } else {
      return [k, v.toString()]
    }
  })
  .filter(([k, v]) => v !== undefined)

How can I filter out values within an array-of-arrays in such a way that TypeScript will recognize the type restrictions?

2

There are 2 best solutions below

1
On BEST ANSWER

This has less to do with the order and rather with the fact you are using a narrowing if statement which allows TS to infer this properly. IE, it will work in this order as well.

Object.entries(props)
  .filter(([k, v]) => v !== undefined)
  .map(([k, v]) => {
    if (v === undefined) {
      return [k, v]
    } else {
      return [k, v.toString()]
    }
  })

Filter assumes its return value to always match the same shape as its predicate, this is just simply a limitation of how it was typed. TS can not evaluate compile-code across scope-boundaries very well. Nor does it try to, for various reasons, IIRC there are many cases in which a module may be augmented (thus changing the implementation), alternatively you can augment (and/or overload) the declaration yourself if you prefer to. https://www.typescriptlang.org/docs/handbook/declaration-merging.html#module-augmentation, OR to declare a different type only syntax here, ie. declare global {...}, using your own type generics to describe the return value.

But there is not much point to, since it will be equally as verbose as the answer below

You can cast it using as

(Object.entries(props)
    .filter(([k, v]) => v !== undefined) as [string, string][])
    .map(([k, v]) => [k, v.toString()])

View this on TS Playground

0
On

Less verbose way is to use an assertion:

      Object.entries(props)
        .filter(([k, v]) => v !== undefined)
        .map(([k, v]) => [k, v!.toString()])

! serves as a non-nullish assertion:

let nullable: string | undefined = "hello world";

nullable!.slice(0, 5); // "hello", no error