Based on the code in your linked playground, TypeScript is correctly inferring your types. The issue arises from a misunderstanding of TypeScript due to this line (*per my understanding of your true intentions):
type Widget = WidgetChart | WidgetBase;
Specifically, considering WidgetBase
's definition (from your linked playground),
enum WidgetType {
CHART = 'chart',
TABLE = 'table'
}
interface WidgetBase {
type: WidgetType
title: string
}
An object like
{ type: 'chart', title: 'some string' }
is a valid WidgetBase. Since
Widget = WidgetChart | WidgetBase
, this object also qualifies as a valid "Widget". It matches the case where
type === 'chart'
and doesn't include a reference to
chartType
. Hence, TypeScript rightly warns that
chartType
might not always exist on a Widget with a type of 'chart'.
To rectify this, you must provide TS only with the actual WidgetTypes:
enum WidgetType {
CHART = 'chart',
TABLE = 'table'
}
enum ChartType {
LINE = 'line',
BAR = 'bar'
}
interface WidgetBase {
title: string
// ... other common, *generic* properties ...
}
interface WidgetTable extends WidgetBase {
type: WidgetType.TABLE
columnWidth: number
// ... other specific properties ...
}
interface WidgetChart extends WidgetBase {
type: WidgetType.CHART,
chartType: ChartType
// ... other specific properties ...
}
type Widget = WidgetChart | WidgetTable; // <-- There are only two choices now: either a valid WidgetChart or a valid WidgetTable, nothing else! You can add more WidgetTypes here in a similar manner of course if you want
function renderWidget(widget: Widget){
switch(widget.type) {
case WidgetType.CHART:
return console.log(widget.chartType); // this works now and doesn't complain :)
case WidgetType.TABLE:
return console.log(widget.columnWidth); // so does this! :)
// you don't _need_ a default here in this case, since you've covered all the WidgetTypes
}
}
Edit: An alternative:
If you have several widget types with matching properties (except for the type) and only a few needing extra properties, consider a different approach. For instance, widgets with identical properties but varying types could be styled differently, e.g., type = 'card-big' or type = 'card-small' etc.
In such cases, manually defining the extensions to WidgetBase
for each type may not be feasible. Instead, employing Exclude
and Omit
from the TS Utility Types could simplify things. Here's how the code would look:
enum WidgetType {
CHART = 'chart',
TABLE = 'table',
CARD_BIG = 'card-big',
CARD_SMALL = 'card-small',
// ... etc ...
}
interface WidgetBase {
type: Exclude<WidgetType, WidgetType.CHART | WidgetType.TABLE>, // <-- A generic widget characterizes any widget *besides* chart or table
title: string
// ... other common, *generic* properties ...
}
interface WidgetTable extends Omit<WidgetBase, 'type'> { // <-- Extend base properties but omit clashing type
type: WidgetType.TABLE
columnWidth: number
// ... other specific properties ...
}
interface WidgetChart extends Omit<WidgetBase, 'type'> {
type: WidgetType.CHART,
chartType: ChartType
// ... other specific properties ...
}
type Widget = WidgetChart | WidgetTable | WidgetBase; // <-- Specify special cases + generic case without ambiguity
function renderWidget(widget: Widget) {
switch(widget.type) {
case WidgetType.CHART:
return console.log(widget.title, widget.chartType); // Specific widgets hold both generic and specific properties
case WidgetType.TABLE:
return console.log(widget.title, widget.columnWidth); // Same as above
// ... Handle other generic widgets if needed, or ...
default:
return console.log(widget.title) // Other widgets will feature only generic properties
}
}