Copié dos nombres de método de la librería que iba a sustituir. Fue un error, y tardé unas horas en verlo.
Mantengo ng-hub-ui-signature, un campo de firma para formularios Angular. Su principal alternativa, angular2-signaturepad, publicó su última versión en febrero de 2022 y sigue moviendo unas catorce mil descargas semanales. Mucha gente acabará migrando, y cuanto más se parezcan las dos APIs, menos dolerá.
Con esa idea añadí toData() y fromData() a la mía, tal cual se llaman allí.
El problema es lo que devuelve cada una
Esto es lo que declara angular2-signaturepad en sus tipos publicados:
export interface Point {
x: number;
y: number;
time: number;
}
export declare type PointGroup = Array<Point>;
toData(): Array<PointGroup>;
fromData(points: Array<PointGroup>): void;
Y esto lo que declaraba la mía:
export interface HubSignaturePoint {
x: number;
y: number;
pressure: number;
}
export interface HubSignatureStroke {
points: HubSignaturePoint[];
color: string;
width: number;
}
toData(): HubSignatureStroke[];
fromData(strokes: readonly HubSignatureStroke[]): void;
Una es un array de arrays de puntos con marca de tiempo. La otra es un objeto con sus puntos, su color y su grosor, y en vez de time guarda pressure.
Mismo nombre, misma aridad, forma incompatible.
Por qué TypeScript no te salva
Si quien migra tiene el valor tipado, el compilador le para los pies y el error aparece en el sitio correcto. Pero una firma guardada no suele vivir en una variable tipada: viene de una columna de base de datos, de una respuesta de API o de un localStorage, y llega como any.
// Lo que tenía guardado, cargado desde la API
const stored = await this.api.getSignature(id); // any
// Antes
this.pad().fromData(stored); // dibujaba la firma
// Después de cambiar de librería, con los mismos nombres
this.pad().fromData(stored); // compila, no dibuja nada, no avisa
Ahí no salta nada. fromData() acepta la estructura antigua, recorre lo que no debe, no dibuja nada reconocible y no lanza ningún error. El fallo aparece cuando un usuario abre un contrato firmado y ve el recuadro vacío.
La solución fue romper a propósito
Los renombré a toStrokes() y fromStrokes(). Ahora quien copie su código antiguo recibe esto:
Property 'fromData' does not exist on type 'HubSignatureComponent'.
Did you mean 'fromStrokes'?
Va a la guía de migración, ve la tabla de equivalencias y en treinta segundos sabe que el payload también cambió. He cambiado un fallo silencioso por uno ruidoso, y he hecho la migración ligeramente más incómoda a propósito.
El JSDoc del método lo deja escrito para que nadie lo revierta más adelante por comodidad:
/**
* Deliberately NOT named `toData`, which is what angular2-signaturepad
* calls its equivalent: that one returns Array<Array<{x, y, time}>>
* while this returns {points: [{x, y, pressure}], color, width}[].
* Reusing the name would let a migration compile and fail silently
* wherever the value is typed `any`.
*/
La regla
Conservar el nombre de una API ajena solo compensa cuando conservas también su forma. Si el payload cambia, el nombre heredado deja de ser una cortesía y pasa a ser una trampa que solo se dispara en producción.
La misma regla, dos horas después y al revés
Publiqué la versión con el renombrado a las 15:21. A las 17:08 salió otra, y me tocó aplicar el mismo criterio en sentido contrario.
Esa segunda versión añadía una forma de firmar con el teclado. El campo tenía tabindex="0" desde el principio y no escuchaba más que eventos de puntero: era enfocable e inutilizable, un control obligatorio que ningún usuario de teclado podía rellenar. Al añadir la ruta nueva, los eventos (drawStart) y (drawEnd) pasaron de emitir PointerEvent a emitir esto:
export type HubSignatureDrawEvent = PointerEvent | KeyboardEvent;
Quien hubiera anotado su manejador con el tipo viejo deja de compilar. Podría haberlo dejado en PointerEvent y castear por dentro: nadie se habría enterado, y el evento habría mentido sobre lo que lleva. Salió con su fichero de cambios incompatibles, en una versión menor, con veintiséis descargas semanales a las que avisar.
Es el mismo principio: el sistema de tipos tiene que enterarse del desajuste antes que producción. Da igual que la forma cambie porque heredas una API o porque evolucionas la tuya.
Para quien no programa: al sustituir una herramienta por otra es tentador mantener los nombres de siempre para que nadie tenga que reaprender. Pero si por dentro funcionan distinto, ese parecido hace que el fallo aparezca tarde y sin avisar. A veces conviene que el cambio se note.
