Función signal() y lectura
La función signal() es el punto de entrada fundamental para crear signals en Angular. Esta función, importada desde @angular/core
, nos permite crear contenedores reactivos que encapsulan valores y notifican automáticamente cuando estos cambian.
La sintaxis básica para crear un signal es extremadamente simple. La función signal() acepta un valor inicial como parámetro y devuelve un objeto signal que actúa como getter y setter del valor:
import { Component, signal } from '@angular/core';
@Component({
selector: 'app-user',
standalone: true,
template: `
<h2>{{ username() }}</h2>
<p>Edad: {{ age() }}</p>
`
})
export class UserComponent {
// Crear signals con diferentes tipos de datos
username = signal('Juan Pérez');
age = signal(25);
}
Lectura de valores
Para leer el valor de un signal, simplemente lo invocamos como si fuera una función. Esta característica hace que los signals sean intuitivos de usar, ya que la sintaxis es limpia y expresiva:
// Crear un signal
const contador = signal(0);
// Leer el valor actual
console.log(contador()); // Output: 0
// En templates, la lectura es igual de simple
template: `<span>Contador: {{ contador() }}</span>`
La lectura reactiva es uno de los aspectos más importantes de los signals. Cuando leemos un signal dentro de un contexto reactivo (como un template de componente), Angular automáticamente establece una dependencia y actualizará la vista cuando el valor cambie.
Creación con diferentes tipos de datos
Los signals son fuertemente tipados gracias a TypeScript, y pueden contener cualquier tipo de dato. Angular infiere automáticamente el tipo basándose en el valor inicial:
@Component({
selector: 'app-examples',
standalone: true,
template: `
<div>
<h3>{{ title() }}</h3>
<p>Activo: {{ isActive() ? 'Sí' : 'No' }}</p>
<p>Items: {{ items().length }}</p>
@if (user()) {
<p>Usuario: {{ user()!.name }} - {{ user()!.email }}</p>
}
</div>
`
})
export class ExamplesComponent {
// Signal de string - tipo inferido: WritableSignal<string>
title = signal('Mi Aplicación');
// Signal de boolean - tipo inferido: WritableSignal<boolean>
isActive = signal(true);
// Signal de array - tipo inferido: WritableSignal<string[]>
items = signal(['item1', 'item2', 'item3']);
// Signal de objeto - tipo inferido: WritableSignal<User | null>
user = signal<{name: string, email: string} | null>({
name: 'Ana García',
email: 'ana@email.com'
});
// Signal de número - tipo inferido: WritableSignal<number>
score = signal(100);
}
Tipado explícito
Aunque Angular infiere los tipos automáticamente, podemos especificar tipos explícitamente cuando necesitamos mayor control:
interface Product {
id: number;
name: string;
price: number;
}
@Component({
selector: 'app-shop',
standalone: true,
template: `
@if (selectedProduct()) {
<div>
<h3>{{ selectedProduct()!.name }}</h3>
<p>Precio: {{ selectedProduct()!.price }}€</p>
</div>
}
<p>Total productos: {{ products().length }}</p>
`
})
export class ShopComponent {
// Tipado explícito para mayor claridad
selectedProduct = signal<Product | null>(null);
// Array de productos con tipo explícito
products = signal<Product[]>([
{ id: 1, name: 'Laptop', price: 999 },
{ id: 2, name: 'Mouse', price: 25 }
]);
// Signal de unión de tipos
status = signal<'loading' | 'success' | 'error'>('loading');
}
Lectura en métodos del componente
La lectura de signals en métodos del componente funciona igual que en templates. Simplemente invocamos el signal como función:
@Component({
selector: 'app-calculator',
standalone: true,
template: `
<div>
<p>Valor A: {{ valueA() }}</p>
<p>Valor B: {{ valueB() }}</p>
<p>Suma: {{ calculateSum() }}</p>
<button (click)="logValues()">Mostrar valores</button>
</div>
`
})
export class CalculatorComponent {
valueA = signal(10);
valueB = signal(20);
calculateSum(): number {
// Leer signals dentro de métodos
return this.valueA() + this.valueB();
}
logValues(): void {
// Acceder a valores actuales de los signals
console.log('Valor A:', this.valueA());
console.log('Valor B:', this.valueB());
console.log('Suma:', this.calculateSum());
}
}
Signals con valores complejos
Los signals pueden contener estructuras de datos complejas como objetos anidados, arrays de objetos, o cualquier tipo de dato JavaScript:
interface UserPreferences {
theme: 'light' | 'dark';
notifications: {
email: boolean;
push: boolean;
};
language: string;
}
@Component({
selector: 'app-settings',
standalone: true,
template: `
<div>
<h3>Configuración de Usuario</h3>
<p>Tema: {{ preferences().theme }}</p>
<p>Email: {{ preferences().notifications.email ? 'Activado' : 'Desactivado' }}</p>
<p>Push: {{ preferences().notifications.push ? 'Activado' : 'Desactivado' }}</p>
<p>Idioma: {{ preferences().language }}</p>
</div>
`
})
export class SettingsComponent {
preferences = signal<UserPreferences>({
theme: 'light',
notifications: {
email: true,
push: false
},
language: 'es'
});
getCurrentTheme(): string {
// Acceder a propiedades anidadas
return this.preferences().theme;
}
}
La simplicidad de la lectura hace que los signals sean especialmente atractivos para desarrolladores que buscan un código limpio y expresivo. No hay necesidad de suscripciones manuales, ni de gestionar observables complejos para casos de uso básicos. Simplemente creamos el signal con signal()
y lo leemos invocándolo como función, y Angular se encarga automáticamente de toda la reactividad.
Tipos WritableSignal y Signal
En el sistema de types de Angular, los signals se dividen en dos interfaces principales que definen claramente las capacidades de cada signal: WritableSignal<T>
y Signal<T>
. Esta distinción tipográfica es fundamental para entender el comportamiento y las restricciones de los signals en diferentes contextos.
WritableSignal
El tipo WritableSignal representa signals que pueden ser tanto leídos como modificados. Cuando creamos un signal usando la función signal()
, obtenemos siempre un WritableSignal
:
import { WritableSignal, signal } from '@angular/core';
@Component({
selector: 'app-counter',
standalone: true,
template: `
<div>
<p>Contador: {{ count() }}</p>
<button (click)="increment()">Incrementar</button>
</div>
`
})
export class CounterComponent {
// Tipo explícito: WritableSignal<number>
count: WritableSignal<number> = signal(0);
increment(): void {
// Podemos modificar porque es WritableSignal
this.count.set(this.count() + 1);
}
}
Los WritableSignal proporcionan métodos adicionales para la modificación del estado, como set()
y update()
, que no están disponibles en signals de solo lectura. El tipo incluye toda la funcionalidad de lectura más las capacidades de escritura.
Signal
El tipo Signal representa signals de solo lectura. Estos signals pueden ser leídos pero no modificados directamente. Este tipo es especialmente útil cuando queremos exponer datos de forma reactiva sin permitir modificaciones externas:
import { Signal, WritableSignal, signal } from '@angular/core';
@Component({
selector: 'app-user-profile',
standalone: true,
template: `
<div>
<h2>{{ displayName() }}</h2>
<p>Email: {{ email() }}</p>
<input [value]="email()" (input)="updateEmail($event)" />
</div>
`
})
export class UserProfileComponent {
// WritableSignal privado para el estado interno
private _email: WritableSignal<string> = signal('usuario@email.com');
private _firstName: WritableSignal<string> = signal('Juan');
private _lastName: WritableSignal<string> = signal('Pérez');
// Signal público de solo lectura para exposición externa
email: Signal<string> = this._email;
// Computed signal también es de tipo Signal (solo lectura)
displayName: Signal<string> = computed(() =>
`${this._firstName()} ${this._lastName()}`
);
updateEmail(event: Event): void {
const target = event.target as HTMLInputElement;
// Solo podemos modificar el WritableSignal privado
this._email.set(target.value);
}
}
La forma habitual de crear signals de solo lectura (Signal) es con la función
computed()
, que la exploraremos en una lección dedicada a ello.
Diferencias fundamentales
La diferencia principal entre ambos tipos radica en las operaciones permitidas:
@Component({
selector: 'app-type-demo',
standalone: true,
template: `<div>{{ demonstration() }}</div>`
})
export class TypeDemoComponent {
// WritableSignal: lectura + escritura
writableSignal: WritableSignal<string> = signal('Editable');
// Signal: solo lectura (computed signal)
readOnlySignal: Signal<string> = computed(() =>
`Procesado: ${this.writableSignal()}`
);
demonstration(): string {
// ✅ Lectura permitida en ambos tipos
const writableValue = this.writableSignal();
const readOnlyValue = this.readOnlySignal();
// ✅ Escritura permitida solo en WritableSignal
this.writableSignal.set('Nuevo valor');
// ❌ Error de TypeScript: Signal no tiene métodos set/update
// this.readOnlySignal.set('Error'); // Property 'set' does not exist
return `${writableValue} - ${readOnlyValue}`;
}
}
Uso en APIs públicas
Los tipos de signals son especialmente importantes al diseñar APIs de componentes. Es una buena práctica exponer signals como Signal<T>
(solo lectura) para evitar modificaciones accidentales desde componentes externos:
interface UserData {
id: number;
name: string;
isActive: boolean;
}
@Component({
selector: 'app-user-service',
standalone: true,
template: `
<div>
<h3>{{ currentUser()?.name || 'No user selected' }}</h3>
<p>Status: {{ userStatus() }}</p>
</div>
`
})
export class UserServiceComponent {
// Estado interno privado (WritableSignal)
private _currentUser: WritableSignal<UserData | null> = signal(null);
private _isLoading: WritableSignal<boolean> = signal(false);
// API pública de solo lectura (Signal)
currentUser: Signal<UserData | null> = this._currentUser;
isLoading: Signal<boolean> = this._isLoading;
// Computed signal automáticamente es Signal<T>
userStatus: Signal<string> = computed(() => {
if (this.isLoading()) return 'Cargando...';
if (!this.currentUser()) return 'Sin usuario';
return this.currentUser()!.isActive ? 'Activo' : 'Inactivo';
});
// Métodos públicos para modificar el estado
loadUser(userData: UserData): void {
this._isLoading.set(true);
// Simulación de carga asíncrona
setTimeout(() => {
this._currentUser.set(userData);
this._isLoading.set(false);
}, 1000);
}
clearUser(): void {
this._currentUser.set(null);
}
}
Asignación de tipos
Angular permite asignaciones seguras entre estos tipos. Un WritableSignal<T>
puede asignarse a un Signal<T>
porque incluye toda la funcionalidad de lectura, pero no al revés:
@Component({
selector: 'app-assignment-demo',
standalone: true,
template: `<p>{{ value() }}</p>`
})
export class AssignmentDemoComponent {
private writableSignal: WritableSignal<number> = signal(42);
// ✅ Válido: WritableSignal se puede asignar a Signal
readOnlyReference: Signal<number> = this.writableSignal;
// ❌ Error: Signal no se puede asignar a WritableSignal
// writableReference: WritableSignal<number> = this.readOnlyReference;
value(): number {
// Ambos se leen de la misma manera
return this.readOnlyReference();
}
}
Parámetros de funciones
Los tipos de signals son especialmente útiles al definir funciones que reciben signals como parámetros:
@Component({
selector: 'app-function-params',
standalone: true,
template: `
<div>
<p>{{ processReadOnly() }}</p>
<p>{{ processWritable() }}</p>
</div>
`
})
export class FunctionParamsComponent {
private data = signal('Datos iniciales');
// Función que solo necesita leer el signal
private formatData(source: Signal<string>): string {
return `Formateado: ${source()}`;
}
// Función que necesita modificar el signal
private updateAndFormat(target: WritableSignal<string>, newValue: string): string {
target.set(newValue);
return `Actualizado: ${target()}`;
}
processReadOnly(): string {
// ✅ WritableSignal puede pasarse como Signal
return this.formatData(this.data);
}
processWritable(): string {
// ✅ Solo WritableSignal puede pasarse como WritableSignal
return this.updateAndFormat(this.data, 'Nuevo valor');
}
}
Esta distinción tipográfica proporciona seguridad en tiempo de compilación y hace que las intenciones del código sean más claras. Al usar Signal<T>
en APIs públicas, comunicamos que el valor es reactivo pero inmutable desde el exterior, mientras que WritableSignal<T>
indica que se espera la capacidad de modificación.

Alan Sastre
Ingeniero de Software y formador, CEO en CertiDevs
Ingeniero de software especializado en Full Stack y en Inteligencia Artificial. Como CEO de CertiDevs, Angular es una de sus áreas de expertise. Con más de 15 años programando, 6K seguidores en LinkedIn y experiencia como formador, Alan se dedica a crear contenido educativo de calidad para desarrolladores de todos los niveles.
Más tutoriales de Angular
Explora más contenido relacionado con Angular y continúa aprendiendo con nuestros tutoriales gratuitos.
Aprendizajes de esta lección
- Comprender la función signal() para crear signals reactivos en Angular.
- Aprender a leer valores de signals tanto en templates como en métodos de componentes.
- Diferenciar entre WritableSignal y Signal, y sus usos respectivos.
- Aplicar tipado explícito e inferido en signals para distintos tipos de datos.
- Entender la importancia de exponer signals de solo lectura en APIs públicas para mantener la integridad del estado.