I would argue TS is in fact correctly inferring your types based on the code in your linked playground. Your issue is that you are "confusing" TypeScript with this line (*confusing based on what I understand your real intentions to be):
type Widget = WidgetChart | WidgetBase;
Specifically, based on WidgetBase's definition (in your linked playground),
enum WidgetType {
CHART = 'chart',
TABLE = 'table'
}
interface WidgetBase {
type: WidgetType
title: string
}
{ type: 'chart', title: 'some string' } would be a valid WidgetBase and because Widget = WidgetChart | WidgetBase this object would also be a valid "Widget"! This object matches the switch case of type === 'chart' and does not contain a reference to chartType. So TypeScript is "correctly" warning you that chartType may not always exist on a Widget with a type of 'chart' (because of the way you have defined Widget).
Therefore, to correct it, you would need to provide TS with the actual WidgetTypes only, i.e.:
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:
Assuming you have many different widget types with identical properties (other than the type) and only a few which need additional properties, you could take a slightly different approach. Just for the sake of the example, the widgets with the same properties but different type would be rendered with different styling, e.g.: type = 'card-big' or type = 'card-small' etc.
In this case you might not want to have to specify every one of the empty extensions to the WidgetBase that you would have to do using the approach above. Instead you could use the Exclude and Omit TS Utility Types. The working code would then look something like this:
enum WidgetType {
CHART = 'chart',
TABLE = 'table',
CARD_BIG = 'card-big',
CARD_SMALL = 'card-small',
// ... etc ...
}
interface WidgetBase {
type: Exclude<WidgetType, WidgetType.CHART | WidgetType.TABLE>, // <-- this is the important bit - A generic widget is any widget *other than* chart or table
title: string
// ... other common, *generic* properties ...
}
interface WidgetTable extends Omit<WidgetBase, 'type'> { // <-- extend the base properties but omit the type which would clash otherwise
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 the special cases + the generic case without confusing TS because WidgetBase no longer overlaps with WidgetChart etc.
function renderWidget(widget: Widget) {
switch(widget.type) {
case WidgetType.CHART:
return console.log(widget.title, widget.chartType); // your specific widgets have both generic and specific properties
case WidgetType.TABLE:
return console.log(widget.title, widget.columnWidth); // same as above
// ... handle other generic widgets if you want, or ...
default:
return console.log(widget.title) // all your other widgets would have only the generic properties
}
}