I have a dynamic table component in Angular 19, which is built using a TableConfig
object. This configuration creates the table's structure, including headers, columns, and rows. The rows
property of TableConfig
can contain an array of SupportTheme
or Role
objects.
My issue is that even if I check in my code that .roles
exists, an error occurs when I attempt to access the roles
property on a SupportTheme
object. Although I have an if
condition that ensures this code block is only executed when tconfig.type
is 'SUPPORT_THEMES'
, the Angular compiler still throws the following error:
NG9: Property 'roles' does not exist on type 'SupportTheme | Role'.
Property 'roles' does not exist on type 'Role'. [plugin angular-compiler]
This error doesn't make sense to me because the if
condition should prevent any Role
object from being processed in this block of code. (Also tried obj.hasOwnProperty('roles')
= same result)
<!-- Row Content -->
@for (obj of tconfig.rows; track obj.id) {
<tr class="border-b dark:border-gray-700 transition-colors duration-300">
@if (tconfig.type === 'SUPPORT_THEMES') {
<td class="px-3 py-3 text-sm text-gray-500">
{{ obj.icon }}
</td>
}
<th scope="row" class="px-4 py-3 font-medium text-gray-900 whitespace-nowrap dark:text-white
transition-colors duration-300 flex items-center">
<div class="w-3 h-3 rounded-full inline-block me-1.5"
[style.background-color]="obj.type === 'TEAMLIST' ? '#' + obj.color.toString(16).padStart(6, '0') : ''"></div>
{{ obj.name }}
</th>
@if (tconfig.type === 'TEAMLIST') {
@if (obj.support_level) {
<td class="px-4 py-3">{{ getSupportLevel(obj.support_level) }}</td>
}
} @else if (tconfig.type === 'SUPPORT_THEMES') { // I also tried 'obj.hasOwnProperty('roles')'
@for (role of obj.roles; track role.id) {
<span class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-opacity-10
bg-blue-500 text-blue-700 dark:bg-opacity-20 dark:bg-blue-400 dark:text-blue-200
border border-blue-200 dark:!border-blue-600 shadow-sm hover:bg-opacity-20 hover:shadow
transition-all duration-200">
{{ role.name }}
</span>
}
}
</tr>
}
Relevant data types:
export interface TableConfig {
type: 'SUPPORT_THEMES' | 'TEAMLIST';
list_empty: string;
dataLoading: boolean;
columns: ColumnConfig[];
rows: SupportTheme[] | Role[];
action_btn: ButtonConfig[];
}
export interface SupportTheme {
id: string;
name: string;
icon: string;
roles: Role[];
}
export interface Role {
id: string;
name: string;
// .. other attributes .. no "roles" here ..
// properties added by us
support_level?: number;
}
You can leverage User Defined Type Guards - Type Predicates to ensure that the proper type is applied.
Below we use the properties that differ between the two types to identify the typing to apply.
isSupportType(obj: SupportTheme | Role): obj is SupportTheme {
return (obj as SupportTheme).roles !== undefined;
}
isRoleType(obj: SupportTheme | Role): obj is Role {
return (obj as Role).support_level !== undefined;
}
When we use this function inside an @if
condition, the type which you set after the is
(obj is SupportTheme
) is taken.
} @else if (tconfig.type === 'SUPPORT_THEMES' && isSupportType(obj)) {
@for (role of obj.roles; track role.id) {
<span class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-opacity-10
bg-blue-500 text-blue-700 dark:bg-opacity-20 dark:bg-blue-400 dark:text-blue-200
border border-blue-200 dark:!border-blue-600 shadow-sm hover:bg-opacity-20 hover:shadow
transition-all duration-200">
{{ role.name }}
</span>
}
import { Component } from '@angular/core';
import { bootstrapApplication } from '@angular/platform-browser';
export interface ColumnConfig {}
export interface ButtonConfig {}
export interface TableConfig {
type: 'SUPPORT_THEMES' | 'TEAMLIST';
list_empty: string;
dataLoading: boolean;
columns: ColumnConfig[];
rows: SupportTheme[] | Role[];
action_btn: ButtonConfig[];
}
export interface SupportTheme {
id: string;
name: string;
icon: string;
roles: Role[];
color: string;
type: string;
}
export interface Role {
id: string;
name: string;
support_level?: number;
}
@Component({
selector: 'app-root',
template: `
<table>
<!-- Row Content -->
@for (obj of tconfig.rows; track obj.id) {
<tr class="border-b dark:border-gray-700 transition-colors duration-300">
@if(isSupportType(obj)) {
@if (tconfig.type === 'SUPPORT_THEMES') {
<td class="px-3 py-3 text-sm text-gray-500">
{{ obj.icon }}
</td>
}
<th scope="row" class="px-4 py-3 font-medium text-gray-900 whitespace-nowrap dark:text-white
transition-colors duration-300 flex items-center">
<div class="w-3 h-3 rounded-full inline-block me-1.5"
[style.background-color]="obj.type === 'TEAMLIST' ? '#' + obj.color.toString().padStart(6, '0') : ''"></div>
{{ obj.name }}
</th>
}
@if (tconfig.type === 'TEAMLIST' && isRoleType(obj)) {
@if (obj.support_level) {
<td class="px-4 py-3">{{ getSupportLevel(obj.support_level) }}</td>
}
} @else if (tconfig.type === 'SUPPORT_THEMES' && isSupportType(obj)) {
@for (role of obj.roles; track role.id) {
<span class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-opacity-10
bg-blue-500 text-blue-700 dark:bg-opacity-20 dark:bg-blue-400 dark:text-blue-200
border border-blue-200 dark:!border-blue-600 shadow-sm hover:bg-opacity-20 hover:shadow
transition-all duration-200">
{{ role.name }}
</span>
}
}
</tr>
}
</table>
`,
})
export class App {
name = 'Angular';
tconfig: TableConfig = {
type: 'SUPPORT_THEMES',
list_empty: 'test',
dataLoading: false,
columns: [],
action_btn: [],
rows: [
{
id: '1',
icon: 'test',
name: 'qwer',
roles: [],
color: '#000000',
type: 'asdf',
},
],
};
isSupportType(obj: SupportTheme | Role): obj is SupportTheme {
return (obj as SupportTheme).roles !== undefined;
}
isRoleType(obj: SupportTheme | Role): obj is Role {
return (obj as Role).support_level !== undefined;
}
getSupportLevel(test: any) {
return '';
}
}
bootstrapApplication(App);