If you want TS to understand that all references to an potentially empty object below a certain check are indeed that full type and not the unset empty object, you likely want an emptiness check that determines if the value is Record<string, never>,
function isEmpty<T>(x: T | Record<string, never>): x is Record<string, never> {
return x != null &&
typeof x === 'object' &&
Object.keys(x).length === 0
}
A common problem here is when JS logic has determined a nested object, usually in state, has been set, but TS complains, e.g.
import { isEmpty } from 'ramda'
// typed as (x: any) => boolean <--- boolean needs to be narrowed
const [user, setUser] = useState({})
...
const Dashboard = ({user}) => {
if(isEmpty(user)) return null
user.address.city // <--- city is there, but TS complains
If you type your object as T | Record<string, never> and write an emptiness check like the one above that types the return as value is Record<string, never>, TS will not complain in the above code and recognizes the entire user to as set.
Emptiness checks are usually generalized, so here are some other types that might be helpful in checking empty objects, arrays, or strings.
type Empty<T> =
T extends Array<infer U> ? [] :
T extends object ? Record<string, never> :
T extends string ? '' :
never
type MaybeEmpty<T> = T | Empty<T>
Note the return type for an empty array is x is never[], but I find that getting TS to understand emptiness is usually just an issue with objects.
userto be of typeUser | {}orPartial<User>, or you need to redefine theUsertype to allow an empty object. Right now, the compiler is correctly telling you thatuseris not aUser.Record<string, never>: typescript-eslint.io/rules/no-empty-object-type