Search code examples
typescripttypeguards

Why typescript typeguard doesn't work for an internal property of an object


Using typescript, when a function returns an object that has properties that might be null. Why does using a typeguard on these inner properties not allow typescript to infer that the inner prop must not be null after the guard?

Here's a minimum example of this. Try it

interface DatabaseResponse {
  settings: string | null
}
interface MainResponse {
  settings: string
}

const retrieveFromDatabase = (): DatabaseResponse => {
  return {
    settings: 'always a string but lets pretend it could be null sometimes'
  }
}

const main = (): MainResponse | Error => {
  const data = retrieveFromDatabase()

  if (data.settings === null) {
    throw new Error()
  }

  return data

}

The error for the return of the main function is

Type 'DatabaseResponse' is not assignable to type 'MainResponse | Error'.
  Type 'DatabaseResponse' is not assignable to type 'MainResponse'.
    Types of property 'settings' are incompatible.
      Type 'string | null' is not assignable to type 'string'.
        Type 'null' is not assignable to type 'string'.

Solution

  • That's not really a type guard. You can do this instead:

    interface DatabaseResponse {
        settings: string | null
    }
    interface MainResponse {
        settings: string
    }
    
    const retrieveFromDatabase = (): DatabaseResponse => {
        return {
            settings: 'always a string but lets pretend it could be null sometimes'
        } as DatabaseResponse
    }
    
    const main = (): MainResponse | Error => {
        const data = retrieveFromDatabase()
    
        if (!isMainResponse(data)) {
            throw new Error()
        }
    
        return data
    }
    
    const isMainResponse = (data: DatabaseResponse | MainResponse): data is MainResponse {
      return !!data.settings;
    }
    

    Or similarly, create a new nonNullable type with the same typeguard

    type NonNullableDB = { [K in keyof DatabaseResponse]: NonNullable<DatabaseResponse[K]> }
    
    const isMainResponse = (data: DatabaseResponse | MainResponse): data is NonNullableDB => {
        return !!data.settings;
    }