Inyección de Dependencias Avanzada en Angular 2026: Providers, Tokens y Preguntas de Entrevista

Dominar la inyección de dependencias en Angular con InjectionToken, inyectores jerárquicos, modificadores de resolución y multi-providers. Incluye preguntas de entrevista y patrones de producción.

Inyección de Dependencias Avanzada en Angular 2026

El sistema de inyección de dependencias de Angular proporciona un control detallado sobre la instanciación de servicios a través de providers, tokens e inyectores jerárquicos. Dominar estos conceptos separa a los desarrolladores Angular experimentados de los juniors en entrevistas técnicas y bases de código en producción.

Concepto Clave

Angular mantiene dos árboles de inyectores paralelos: el árbol ModuleInjector para servicios proporcionados a nivel de módulo, y el árbol ElementInjector para dependencias con alcance de componente. La resolución comienza a nivel de elemento y sube hasta la raíz.

Estrategias de Configuración de Providers en Angular DI

Angular ofrece múltiples estrategias de configuración de providers, cada una adecuada para diferentes casos de uso. Las configuraciones más comunes usan useClass, useValue, useFactory y useExisting.

app.config.tstypescript
import { ApplicationConfig, InjectionToken } from '@angular/core';
import { LoggerService } from './services/logger.service';
import { DebugLoggerService } from './services/debug-logger.service';
import { API_CONFIG, ApiConfig } from './config/api.config';

export const appConfig: ApplicationConfig = {
  providers: [
    // useClass: proporcionar una implementación diferente
    { provide: LoggerService, useClass: DebugLoggerService },
    
    // useValue: proporcionar un objeto de configuración estático
    { 
      provide: API_CONFIG, 
      useValue: { baseUrl: 'https://api.example.com', timeout: 5000 } 
    },
    
    // useFactory: crear dependencia con lógica en tiempo de ejecución
    {
      provide: 'FEATURE_FLAGS',
      useFactory: () => {
        const env = import.meta.env.MODE;
        return { debugMode: env === 'development', analytics: env === 'production' };
      }
    },
    
    // useExisting: crear un alias hacia otro provider
    { provide: 'Logger', useExisting: LoggerService }
  ]
};

La estrategia useClass intercambia implementaciones sin cambiar el código consumidor. La estrategia useValue proporciona objetos estáticos como configuración. La estrategia useFactory maneja decisiones en tiempo de ejecución, y useExisting crea alias para acceso polimórfico.

InjectionToken para Dependencias No-Clase con Type Safety

Mientras que @Injectable funciona para servicios basados en clases, los valores no-clase como objetos de configuración, primitivos o funciones requieren InjectionToken. Este token actúa como una clave única en el registro DI de Angular.

tokens/config.tokens.tstypescript
import { InjectionToken } from '@angular/core';

export interface ApiConfig {
  baseUrl: string;
  timeout: number;
  retryAttempts: number;
}

// El parámetro genérico asegura type safety en el punto de inyección
export const API_CONFIG = new InjectionToken<ApiConfig>('api.config', {
  providedIn: 'root',
  factory: () => ({
    baseUrl: 'https://api.sharpskill.dev',
    timeout: 30000,
    retryAttempts: 3
  })
});

// Token para valores primitivos
export const MAX_UPLOAD_SIZE = new InjectionToken<number>('max.upload.size', {
  providedIn: 'root',
  factory: () => 10 * 1024 * 1024 // 10MB
});

El parámetro de tipo genérico en InjectionToken<ApiConfig> se propaga a la llamada inject(). TypeScript sabe que el valor inyectado coincide con el tipo declarado, detectando errores de uso en tiempo de compilación en lugar de tiempo de ejecución.

La Función inject() vs Inyección por Constructor

Angular 14 introdujo la función inject() como alternativa a la inyección basada en constructor. En Angular 20+, inject() se ha convertido en el enfoque preferido, especialmente en componentes standalone y contextos funcionales.

user.service.tstypescript
import { Injectable, inject } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { API_CONFIG } from '../tokens/config.tokens';

@Injectable({ providedIn: 'root' })
export class UserService {
  // Enfoque moderno: inject() a nivel de campo
  private readonly http = inject(HttpClient);
  private readonly config = inject(API_CONFIG);

  getUser(id: string) {
    return this.http.get(`${this.config.baseUrl}/users/${id}`);
  }
}

// Alternativa: inyección por constructor (todavía válida)
@Injectable({ providedIn: 'root' })
export class UserServiceLegacy {
  constructor(
    private readonly http: HttpClient,
    @Inject(API_CONFIG) private readonly config: ApiConfig
  ) {}
}

La función inject() elimina el boilerplate de decoradores para tokens y habilita la inyección de dependencias en contextos no-clase como guards, resolvers e interceptores funcionales.

auth.guard.tstypescript
import { inject } from '@angular/core';
import { CanActivateFn, Router } from '@angular/router';
import { AuthService } from '../services/auth.service';

// Guard funcional usando inject()
export const authGuard: CanActivateFn = () => {
  const authService = inject(AuthService);
  const router = inject(Router);

  if (authService.isAuthenticated()) {
    return true;
  }
  return router.createUrlTree(['/login']);
};

La función inject() solo funciona dentro de un contexto de inyección: durante la construcción de clase, en funciones factory, o en construcciones funcionales de Angular. Llamarla fuera de estos contextos lanza un error en tiempo de ejecución.

¿Listo para aprobar tus entrevistas de Angular?

Practica con nuestros simuladores interactivos, flashcards y tests técnicos.

Inyectores Jerárquicos: ElementInjector vs EnvironmentInjector

Angular mantiene dos jerarquías de inyectores paralelas que determinan el alcance del servicio y el orden de resolución. Entender esta arquitectura es esencial para controlar el tiempo de vida y la visibilidad de los servicios.

data.service.tstypescript
import { Injectable } from '@angular/core';

// Singleton a nivel raíz: una sola instancia en toda la app
@Injectable({ providedIn: 'root' })
export class GlobalDataService {
  private data = new Map<string, unknown>();
  
  set(key: string, value: unknown) { this.data.set(key, value); }
  get(key: string) { return this.data.get(key); }
}

// Con alcance de componente: nueva instancia por componente
@Injectable()
export class ComponentDataService {
  private data = new Map<string, unknown>();
  
  set(key: string, value: unknown) { this.data.set(key, value); }
  get(key: string) { return this.data.get(key); }
}
dashboard.component.tstypescript
import { Component } from '@angular/core';
import { ComponentDataService } from './services/component-data.service';

@Component({
  selector: 'app-dashboard',
  standalone: true,
  // Este componente y todos sus hijos obtienen la misma instancia
  providers: [ComponentDataService],
  template: `
    <app-widget />
    <app-stats />
  `
})
export class DashboardComponent {}

Cuando un componente declara un provider, Angular crea una nueva instancia con alcance en el ElementInjector de ese componente. Los componentes hijos heredan acceso a los providers del padre a menos que declaren los suyos propios.

El algoritmo de resolución sigue esta ruta:

  1. Verificar el ElementInjector del componente solicitante
  2. Subir por el árbol ElementInjector hasta los ancestros
  3. Verificar el EnvironmentInjector (módulo o providers standalone)
  4. Subir hasta el EnvironmentInjector raíz
  5. Llegar a NullInjector y lanzar un error si no se usó @Optional()

Modificadores de Resolución: @Self, @SkipSelf, @Optional, @Host

Los modificadores de resolución alteran cómo Angular busca en la jerarquía de inyectores. Estos decoradores funcionan tanto con inyección por constructor como con la función inject().

panel.component.tstypescript
import { Component, Optional, SkipSelf, Self, inject } from '@angular/core';
import { PanelService } from './panel.service';

@Component({
  selector: 'app-panel',
  standalone: true,
  providers: [PanelService],
  template: `<ng-content />`
})
export class PanelComponent {
  // @Self: solo buscar en el inyector de este componente
  private readonly localService = inject(PanelService, { self: true });
  
  // @SkipSelf: omitir este componente, iniciar búsqueda desde el padre
  private readonly parentService = inject(PanelService, { 
    skipSelf: true, 
    optional: true 
  });
  
  // @Optional: retornar null en lugar de lanzar error si no se encuentra
  private readonly optionalService = inject(PanelService, { optional: true });

  constructor() {
    // localService es siempre la instancia propia del componente
    // parentService es la instancia del padre o null
    console.log('Local:', this.localService);
    console.log('Parent:', this.parentService);
  }
}

El modificador @Host() restringe la resolución al inyector del elemento host y se detiene en los límites del componente. Esto es útil cuando una directiva necesita acceder a un servicio proporcionado por su componente host pero no debe alcanzar más arriba en el árbol.

highlight.directive.tstypescript
import { Directive, inject, Host, Optional } from '@angular/core';
import { HighlightConfig } from './highlight.config';

@Directive({
  selector: '[appHighlight]',
  standalone: true
})
export class HighlightDirective {
  // Solo buscar en los providers del componente host
  private readonly config = inject(HighlightConfig, { 
    host: true, 
    optional: true 
  });

  constructor() {
    // config es null si el componente host no proporcionó HighlightConfig
    const color = this.config?.color ?? 'yellow';
    // Aplicar resaltado...
  }
}

Multi-Providers para Sistemas Extensibles

Los multi-providers permiten registrar múltiples valores bajo un solo token. Angular retorna todos los valores registrados como un array, habilitando arquitecturas de plugins y patrones de extensibilidad.

validators.tokens.tstypescript
import { InjectionToken } from '@angular/core';

export interface Validator {
  validate(value: string): string | null;
}

export const VALIDATORS = new InjectionToken<Validator[]>('validators');
app.config.tstypescript
import { ApplicationConfig } from '@angular/core';
import { VALIDATORS } from './validators.tokens';

const requiredValidator = {
  validate: (value: string) => value ? null : 'El campo es requerido'
};

const minLengthValidator = {
  validate: (value: string) => value.length >= 3 ? null : 'Mínimo 3 caracteres'
};

const emailValidator = {
  validate: (value: string) => 
    /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value) ? null : 'Formato de email inválido'
};

export const appConfig: ApplicationConfig = {
  providers: [
    { provide: VALIDATORS, useValue: requiredValidator, multi: true },
    { provide: VALIDATORS, useValue: minLengthValidator, multi: true },
    { provide: VALIDATORS, useValue: emailValidator, multi: true }
  ]
};
validation.service.tstypescript
import { Injectable, inject } from '@angular/core';
import { VALIDATORS, Validator } from './validators.tokens';

@Injectable({ providedIn: 'root' })
export class ValidationService {
  private readonly validators = inject(VALIDATORS);

  validate(value: string): string[] {
    // validators es un array de todos los validadores registrados
    return this.validators
      .map(v => v.validate(value))
      .filter((error): error is string => error !== null);
  }
}

El flag multi: true le indica a Angular que recolecte todos los providers para este token en un array. Sin él, los providers posteriores sobrescribirían a los anteriores.

Preguntas de Entrevista sobre Inyección de Dependencias en Angular

Las entrevistas técnicas frecuentemente evalúan la comprensión del sistema DI de Angular. Estas son preguntas que distinguen a candidatos con experiencia en producción.

P: ¿Qué sucede cuando se proporciona el mismo servicio tanto a nivel de módulo como de componente?

El provider a nivel de componente crea una instancia separada con alcance en el subárbol de ese componente. Los servicios inyectados en ese subárbol reciben la instancia del componente, no el singleton a nivel de módulo. Esto habilita el aislamiento de estado, por ejemplo cuando cada pestaña necesita su propio estado de formulario.

P: ¿Por qué usar InjectionToken en lugar de un string literal?

Los tokens string corren riesgo de colisiones entre bibliotecas o diferentes partes de una aplicación. InjectionToken crea una referencia única en tiempo de ejecución que no puede entrar en conflicto. El parámetro de tipo genérico también proporciona type safety en tiempo de compilación que los tokens string no tienen.

P: ¿Cuándo inject() lanza error vs retorna undefined?

Por defecto, inject() lanza cuando la dependencia no se encuentra. Pasar { optional: true } cambia el tipo de retorno a T | null y retorna null en lugar de lanzar. Esto coincide con el comportamiento del decorador @Optional().

P: Explica la diferencia entre providedIn: 'root' y proporcionar en el array providers de un módulo.

Ambos crean singletons, pero providedIn: 'root' habilita tree-shaking. El servicio solo se incluye en el bundle si realmente se inyecta en algún lugar. Los providers a nivel de módulo siempre se incluyen independientemente del uso.

P: ¿Cómo afecta el lazy loading al alcance del servicio?

Los módulos lazy-loaded obtienen su propio EnvironmentInjector hijo. Los servicios proporcionados en un módulo lazy tienen alcance en ese módulo y sus hijos. Un servicio con providedIn: 'root' permanece como un verdadero singleton a través de todos los módulos, lazy o no.

Para preguntas de práctica sobre servicios Angular y patrones DI, consulta las preguntas de entrevista Angular sobre servicios e inyección de dependencias.

Patrones Prácticos: Configuración y Feature Flags

Las aplicaciones reales combinan estos conceptos DI para gestión de configuración. Este patrón usa InjectionToken, factory providers y conocimiento del entorno.

feature-flags.config.tstypescript
import { InjectionToken, inject, PLATFORM_ID } from '@angular/core';
import { isPlatformBrowser } from '@angular/common';

export interface FeatureFlags {
  newCheckout: boolean;
  darkMode: boolean;
  betaFeatures: boolean;
}

export const FEATURE_FLAGS = new InjectionToken<FeatureFlags>('feature.flags', {
  providedIn: 'root',
  factory: () => {
    const platformId = inject(PLATFORM_ID);
    
    if (!isPlatformBrowser(platformId)) {
      // SSR: retornar valores por defecto seguros
      return { newCheckout: false, darkMode: false, betaFeatures: false };
    }

    // Navegador: verificar localStorage o config remota
    const stored = localStorage.getItem('featureFlags');
    if (stored) {
      return JSON.parse(stored);
    }
    
    return { newCheckout: true, darkMode: false, betaFeatures: false };
  }
});
feature-flag.directive.tstypescript
import { Directive, Input, TemplateRef, ViewContainerRef, inject } from '@angular/core';
import { FEATURE_FLAGS } from './feature-flags.config';

@Directive({
  selector: '[appFeatureFlag]',
  standalone: true
})
export class FeatureFlagDirective {
  private readonly flags = inject(FEATURE_FLAGS);
  private readonly templateRef = inject(TemplateRef<unknown>);
  private readonly viewContainer = inject(ViewContainerRef);

  @Input() set appFeatureFlag(flag: keyof typeof this.flags) {
    if (this.flags[flag]) {
      this.viewContainer.createEmbeddedView(this.templateRef);
    } else {
      this.viewContainer.clear();
    }
  }
}

Esta directiva renderiza condicionalmente contenido basado en feature flags, con la configuración de flags centralizada en un solo token inyectable.

¡Empieza a practicar!

Pon a prueba tu conocimiento con nuestros simuladores de entrevista y tests técnicos.

Prácticas DI Angular Listas para Producción

  • Usar providedIn: 'root' para singletons a nivel de aplicación que se benefician del tree-shaking
  • Preferir inject() sobre inyección por constructor en Angular 20+ para sintaxis más limpia y compatibilidad funcional
  • Acotar servicios con estado a componentes cuando se necesita aislamiento, no a nivel de módulo
  • Crear InjectionToken para dependencias no-clase para asegurar type safety y evitar colisiones
  • Aplicar @Optional() cuando una dependencia podría no existir, especialmente para plugins o características opcionales
  • Probar componentes con providers sobrescritos usando TestBed.overrideComponent() para aislamiento
  • Usar multi-providers para patrones de extensibilidad como validadores, interceptores y handlers
Reto diario

¿Sabrías detectar el bug en Angular?

Un fragmento real, un bug oculto, un intento al día. Sin cuenta para probar.

Anthony Fillion-Maillet

Escrito por

Anthony Fillion-Maillet

Fundador de SharpSkill

Desarrollador fullstack desde hace más de 10 años. Dirige SharpSkill y responde por todo lo que se publica aquí.

Actualizado el 13 de septiembre de 2026

Etiquetas

#angular
#dependency-injection
#typescript
#interview

Compartir

Artículos relacionados