I'm encountering what seems to be unexpected behavior in TypeScript when working with a union type that includes Record<string, never>
(empty object). TypeScript isn't catching potential undefined properties access, which could lead to runtime errors.
Here's a minimal example:
type CatData = {
name: string;
breed: string;
age: number;
};
type MaybeCatData = Record<string, never> | CatData;
// TypeScript doesn't complain about this, but it should!
function processCat(obj: MaybeCatData): CatData {
return {
name: obj.name,
breed: obj.breed,
age: obj.age
};
}
/**
* Returns: {
* "name": undefined,
* "breed": undefined,
* "age": undefined
* }
*/
console.log(processCat({}));
When I define a union type MaybeCatData = Record<string, never> | CatData
, I expect TypeScript to force me to check whether the properties exist before accessing them, since one possibility is an empty object.
However, TypeScript allows direct access to these properties without any type checking, even though accessing properties on an empty object will return undefined.
Is there a better way to type this scenario to force proper type checking?
In your use case (receiving an Express response with two possibilities: an empty body or a cat in its body), you could create this type:
type MaybeCatData = {} | CatData;
This type {}
is problematic in case you're using it to create variables, However, in your case, this isn't an issue because you won't create a MaybeCatData
value yourself, you'll only receive it in a function.
With this type and a type predicate, you can have your safe function, that prevents you from accessing the CatData
properties without first checking the type:
type CatData = {
name: string;
breed: string;
age: number;
};
type MaybeCatData = {} | CatData;
function isCatData(obj: MaybeCatData): obj is CatData {
const keyName: keyof CatData = "name";
return keyName in obj;
}
function processCat(obj: MaybeCatData): CatData {
if (!isCatData(obj)) {
throw new Error("Empty body");
}
return {
name: obj.name,
breed: obj.breed,
age: obj.age
};
}