In short, you cannot access ISample properties on sampleObj because TypeScript cannot inferentially discriminate the {} member of the union ISample | {} since both union members are satisfied by the assignment. Thus, you cannot access a property on sampleObj unless it's present on all members of the union (which rules out all properties).
Unions
From the handbook:
A union type is a type formed from two or more other types, representing values that may be any one of those types. We refer to each of these types as the union’s members.
Remember, unions do not combine types (that's the role of an intersection); they give you options for what a thing (variable, parameter, property, etc.) might be at assignment. For example:
// foo, by annotation, might be a string or a number...
let foo: string | number = 'bar';
// ... but it is assigned to a string, so its realized type is string:
foo.toUpperCase(); // ok, because foo: string
// We can reassign foo as long as it is still a member of the original annotated union
foo = 0;
foo.toUpperCase(); // error, because foo: number
// ^^^^^^^^^^^ Property 'toUpperCase' does not exist on type 'number'
Here based on assignment a narrower type of foo was inferred (or rather, union member string was discriminated).
To re-emphasize, discriminating a union member (at assignment or otherwise) disallows those members until re-assignment, i.e. you can't do this:
let foo: { bar: string } | { baz: number } = { bar: 'foobar' }
foo.baz = 0
// ^^^ Property 'baz' does not exist on type '{ bar: string; }'.
foo = { baz: 0 } // ok
The empty type {}
Typing a thing as {} can lead to some behaviors that feel unexpected but are totally valid.
... the empty type seems to defy expectation:
class Empty {}
function fn(arg: Empty) {
// do something?
}
// No error, but this isn't an 'Empty' ?
fn({ k: 10 });
Consider the following valid TypeScript:
let foo: {} = {};
foo = { bar: 0 };
foo = 'bar';
foo = [ true, 0, '' ];
These are all valid because every property on {} is present on each of these actual types. If we changed it up:
let foo: { length: number } = { length: 0 };
const o = { bar: 0 }
foo = o;
//^^^ Property 'length' is missing in type '{ bar: number; }'
// but required in type '{ length: number; }'
foo = 'bar';
foo = [ true, 0, '' ];
Here, all but one of these satisfy length: number. {} has no properties to satisfy, so anything goes (except null and undefined).
Also note that the {} type is practically useless without type assertions:
let foo: {} = 'bar';
foo.length;
// ^^^^^^ Property 'length' does not exist on type '{}'
Conclusion/Answer
So, looking again at the posted example:
interface ISample {
requiredProp: string
optionalProp?: string
}
const sampleObj: ISample | {} = { requiredProp: "hello" } // object can now be empty
const { requiredProp = "default", optionalProp = "default" } = sampleObj
// ^^^^^^^^^^^^ ^^^^^^^^^^^^
// Property 'requiredProp' does not exist on type '{} | ISample'
// Property 'optionalProp' does not exist on type '{} | ISample'.
console.log(requiredProp, optionalProp)
When assigning the initial value of sampleObj, TypeScript tries to narrow the type by discriminating members that do not match the given value. In this case, it cannot rule out any members of the union since { requiredProp: "hello" } satisfies both ISample and {}.
For the sake of being thorough, note that const { requiredProp = "default", optionalProp = "default" } = sampleObj is trying to access sampleObj.requiredProp and sampleObj.optionalProp, which cannot exist on type {} | ISample. Your options are:
- change
ISample so that it can allow for an empty object (by making all properties optional).
- change the type of
sampleObj to use Partial<ISample>, which accomplishes the same thing without changing ISample's definition.
- always assign the required properties of things typed
ISample to default values.
Regardless of your choice, you should probably remove {} from the union to avoid a headache (though you might look into type predicates).
requiredPropdoesn't have to exist on your object, it's not really required. If you don't know what props you are going to have on the object, use aRecord. If you know that it will be from a selection of properties but want to allow any field to be ommited, usePartial. Otherwise, always provide the required props and don't type it asISample | {}.requiredProphas to exist if the object is not empty. The object can be empty because the app might still be in a fetching state and waiting for data, but when the data comes in it has to match the interface.nullrather than{}. Possibly include a booleanhasLoadedin your state.sampleObjis{}whereinrequiredPropisn't available. With the union you have, you cannot successfully access any properties ofsampleObjwithout asserting a type.