skip to Main Content

I’m struggling a bit with checking if the property value of an object is undefined when I access it dynamically through bracket notation. Below is an example of my code.

function toBritishDate(date: Date | string): string {
    console.log(date)
    return "foo"
}

function myFunc() {
    const project: { start_date?: string; end_date?: string } = {
        start_date: '2023-09-13',
        end_date: '2023-09-29',
    };

    const array: ("start_date" | "end_date")[] = ['start_date', 'end_date']
    array.forEach((element) => {        
        if (project[element] !== undefined) {
            console.log(toBritishDate(project[element]))
        }
   });
}

You can find a playground on this link.

When calling toBritishDate I get the following error on match.project[element]:

Argument of type ‘string | undefined’ is not assignable to parameter of type ‘string | Date’.
Type ‘undefined’ is not assignable to type ‘string | Date’. (tsserver 2345)

It seems like match.project[element] !== undefined only checks if the property exists, it doesn’t check if the actual value is undefined. Does anyone know how I can check if the property exists, and that the value is not undefined?

2

Answers


  1. You are correct in your observation. The type of project[element] is inferred as string | undefined, and when you pass it to toBritishDate, TypeScript is rightly complaining because undefined is not assignable to string | Date as expected by toBritishDate.

    To fix this, you can use optional chaining (?.) to safely access the property and check if the value is defined before calling toBritishDate. Here’s an updated version of your code:

    type ProjectDates = {
      start_date?: string;
      end_date?: string;
    }
    
    const project: ProjectDates = {
      start_date: "2023-09-13",
      end_date: "2023-09-29"
    };
    
    function toBritishDate(dateToFormat: Date | string): string {
      const date: Date = typeof dateToFormat === 'string' ? new Date(dateToFormat) : dateToFormat;
      return new Intl.DateTimeFormat('en-gb', {
        year: 'numeric',
        month: '2-digit',
        day: '2-digit',
        timeZone: 'Europe/Berlin',
      }).format(date);
    }
    
    {['start_date', 'end_date'].map((element) => (
      <div key={element}>
        {project[element] !== undefined
          ? toBritishDate(project[element]!)
          : 'Not important'
        }
      </div>
    ))}
    

    Note the use of the optional chaining (project[element]?.) before accessing the property, and the non-null assertion operator (!) after project[element] to tell TypeScript that you’re certain the value is not undefined at that point. This helps TypeScript understand that you have checked for the existence of the property and the value. Additionally, I added a key prop to the elements for React’s list reconciliation.

    Login or Signup to reply.
  2. This is a known missing feature, originally filed at microsoft/TypeScript#10530 and now currently tracked at microsoft/TypeScript#56389. TypeScript doesn’t keep track of the identity of a key like element when you perform an indexing like project[element]. It only looks at the type, which for element is the union type "start_date" | "end_date". So it analyzes your code as if it were like

    declare const element1: "start_date" | "end_date";
    declare const element2: "start_date" | "end_date";
    if (project[element1] !== undefined) {
            console.log(toBritishDate(project[element2])) // error, of course
    }
    

    which is obviously not safe. TypeScript would need to track the identity of the key to tell the difference between your safe code and the above unsafe code. It can’t, right now, so that’s what’s happening.


    Until and unless that’s addressed, you’ll need to work around it. The standard workaround is to copy your property into a new variable to sidestep the whole issue around indexing and identity:

        const projectElement = project[element];
        if (projectElement !== undefined) {
                console.log(toBritishDate(projectElement)) // okay
        }
    

    Since projectElement is just a single variable, the compiler can just perform regular old narrowing analysis on it, which works as you expect.

    Playground link to code

    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search