Más sobre Funciones en TypeScript
Las funciones son el componente básico de cualquier aplicación, ya sean funciones locales, importadas de otro módulo o métodos de una clase. También son valores y, al igual que otros valores, TypeScript tiene muchas formas de describir cómo se pueden llamar funciones. Aprendamos a escribir tipos que describan funciones.
Expresiones de tipo de función
La forma más sencilla de describir una función es con una expresión de tipo de función. Estos tipos son sintácticamente similares a las funciones de flecha:
La sintaxis (a: string) => void
significa “una función con un parámetro, llamado a
, de tipo string
, que no tiene un valor de retorno”.
Al igual que con las declaraciones de funciones, si no se especifica un tipo de parámetro, implícitamente es any
.
Ten en cuenta que el nombre del parámetro es obligatorio. ¡El tipo de función
(string) => void
significa “una función con un parámetro llamadostring
de tipoany
”!
Por supuesto, podemos usar un alias de tipo para nombrar un tipo de función:
Firmas de llamadas
En JavaScript, las funciones pueden tener propiedades además de ser invocables. Sin embargo, la sintaxis de expresión del tipo de función no permite declarar propiedades. Si queremos describir algo invocable con propiedades, podemos escribir una firma de llamada en un tipo de objeto:
Ten en cuenta que la sintaxis es ligeramente diferente en comparación con una expresión de tipo de función; usa :
entre la lista de parámetros y el tipo de retorno en lugar de =>
.
Firmas de Constructores
Las funciones de JavaScript también se pueden invocar con el operador new
.
TypeScript se refiere a estos como constructores porque normalmente crean un nuevo objeto.
Puedes escribir una firma de constructor agregando la palabra clave new
delante de una firma de llamada:
Algunos objetos, como el objeto Date
de JavaScript, se pueden llamar con o sin new
.
Puedes combinar firmas de llamadas y constructores en el mismo tipo de forma arbitraria:
Funciones Genéricas
Es común escribir una función donde los tipos de entrada se relacionan con el tipo de salida, o donde los tipos de dos entradas están relacionados de alguna manera. Consideremos por un momento una función que devuelve el primer elemento de una array:
Esta función hace su trabajo, pero desafortunadamente tiene el tipo de retorno any
.
Sería mejor si la función devolviera el tipo del elemento del array.
En TypeScript, los generics se usan cuando queremos describir una correspondencia entre dos valores. Hacemos esto declarando un parámetro de tipo en la firma de la función:
Al agregar un parámetro de tipo Type
a esta función y usarlo en dos lugares, hemos creado un vínculo entre la entrada de la función (el array) y la salida (el valor de retorno).
Ahora cuando lo llamamos sale un tipo más específico:
Inferencia
Ten en cuenta que no tuvimos que especificar Type
en este ejemplo.
El tipo fue inferido - elegido automáticamente - por TypeScript.
También podemos usar múltiples parámetros de tipo.
Por ejemplo, una versión independiente de map
se vería así:
Ten en cuenta que en este ejemplo, TypeScript podría inferir tanto el tipo del parámetro de tipo Input
(del array string
dado), como también el tipo de parámetro Output
basado en el valor de retorno de la expresión de la función (number
).
Restricciones
Hemos escrito algunas funciones genéricas que pueden funcionar con cualquier tipo de valor. A veces queremos relacionar dos valores, pero solo podemos operar con un determinado subconjunto de valores. En este caso, podemos usar una restricción para limitar los tipos de tipos que un parámetro de tipo puede aceptar.
Escribamos una función que devuelva el mayor de dos valores.
Para hacer esto, necesitamos una propiedad de length
que sea un número.
Restringimos el parámetro de tipo a ese tipo escribiendo una cláusula extends
:
Hay algunas cosas interesantes a tener en cuenta en este ejemplo.
Permitimos que TypeScript infiera el tipo de retorno longest
.
La inferencia de tipos de retorno también funciona en funciones genéricas.
Debido a que restringimos Type
a { length: number }
, se nos permitió acceder a la propiedad .length
de los parámetros a
y b
.
Sin la restricción de tipo, no podríamos acceder a esas propiedades porque los valores podrían haber sido de otro tipo sin una propiedad length
.
Los tipos de longerArray
y longerString
se infirieron en función de los argumentos.
Recuerda, los generics consisten en relacionar dos o más valores con el mismo tipo.
Finalmente, tal como nos gustaría, la llamada a longest(10, 100)
se rechaza porque el tipo number
no tiene una propiedad .length
.
Trabajar con valores restringidos
Aquí hay un error común cuando se trabaja con restricciones genéricas:
Podría parecer que esta función está bien: Type
está restringido a { length: number }
, y la función devuelve Type
o un valor que coincida con esa restricción.
El problema es que la función promete devolver el mismo tipo de objeto que se pasó, no solo algún objeto que coincida con la restricción.
Si este código fuera legal, podrías escribir código que definitivamente no funcionaría:
Especificar argumentos de tipo
TypeScript generalmente puede inferir los argumentos de tipo deseados en una llamada genérica, pero no siempre. Por ejemplo, digamos que escribiste una función para combinar dos arrays:
Normalmente sería un error llamar a esta función con arrays que no coinciden:
Sin embargo, si tenías la intención de hacer esto, puedes especificar manualmente Type
:
Pautas para escribir buenas funciones genéricas
Escribir funciones genéricas es divertido y puede ser fácil dejarse llevar por los parámetros de tipo. Tener demasiados parámetros de tipo o usar restricciones donde no son necesarias puede hacer que la inferencia sea menos exitosa y frustrar a quienes llaman a tu función.
Empuja los parámetros de tipo hacia abajo
Aquí hay dos formas de escribir una función que parecen similares:
Pueden parecer idénticas a primera vista, pero firstElement1
es una forma mucho mejor de escribir esta función.
Su tipo de retorno inferido es Type
, pero el tipo de retorno inferido de firstElement2
es any
porque TypeScript tiene que resolver la expresión arr[0]
usando la restricción de tipo, en lugar de “esperar” para resolver el elemento durante una llamada.
Regla: Cuando sea posible, usa el parámetro de tipo en sí en lugar de restringirlo
Usa menos parámetros de tipo
Aquí tienes otro par de funciones similares:
Hemos creado un parámetro de tipo Func
que no relaciona dos valores.
Eso siempre es una señal de alerta, porque significa que las personas que invocan y desean especificar argumentos de tipo tienen que especificar manualmente un argumento de tipo adicional sin ningún motivo.
¡Func
no hace nada más que hacer que la función sea más difícil de leer y razonar!
Regla: Utiliza siempre la menor cantidad de parámetros de tipo posible
Los parámetros de tipo deberían aparecer dos veces
A veces olvidamos que es posible que no sea necesario que una función sea genérica:
Fácilmente podríamos haber escrito una versión más simple:
Recuerda, los parámetros de tipo son para relacionar los tipos de múltiples valores.
Si un parámetro de tipo solo se usa una vez en la firma de la función, no relaciona nada.
Esto incluye el tipo de devolución inferido; por ejemplo, si Str
fuera parte del tipo de retorno inferido de greet
, estaría relacionando el argumento y los tipos de retorno, por lo que se usaría dos veces a pesar de aparecer solo una vez en el código escrito.
Regla: si un parámetro de tipo solo aparece en una ubicación, reconsidera seriamente si realmente lo necesitas
Parámetros opcionales
Las funciones en JavaScript a menudo toman una cantidad variable de argumentos.
Por ejemplo, el método toFixed
de number
toma un recuento de dígitos opcional:
Podemos modelar esto en TypeScript marcando el parámetro como opcional con ?
:
Aunque el parámetro se especifica como tipo number
, el parámetro x
en realidad tendrá el tipo number | undefined
porque los parámetros no especificados en JavaScript obtienen el valor undefined
.
También puedes proporcionar un parámetro predeterminado:
Ahora en el cuerpo de f
, x
tendrá el tipo number
porque cualquier argumento undefined
será reemplazado por 10
.
Ten en cuenta que cuando un parámetro es opcional, los invocadores (callers) siempre pueden pasar undefined
, ya que esto simplemente simula un argumento “faltante”:
Parámetros opcionales en Callbacks
Una vez que hayas aprendido acerca de los parámetros opcionales y las expresiones de tipo de función, es muy fácil cometer los siguientes errores al escribir funciones que invocan devoluciones de llamada (callbacks):
Lo que la gente normalmente pretende cuando escribe index?
como parámetro opcional es que quiere que ambas llamadas sean legales:
Lo que esto en realidad significa es que callback
podría invocarse con un argumento.
En otras palabras, la definición de la función dice que la implementación podría verse así:
A su vez, TypeScript aplicará este significado y emitirá errores que en realidad no son posibles:
En JavaScript, si llamas a una función con más argumentos que parámetros, los argumentos adicionales simplemente se ignoran. TypeScript se comporta de la misma manera. Las funciones con menos parámetros (del mismo tipo) siempre pueden reemplazar funciones con más parámetros.
Regla: Al escribir un tipo de función para una devolución de llamada, nunca escribas un parámetro opcional a menos que tengas la intención de llamar la función sin pasar ese argumento
Sobrecargas de funciones
Algunas funciones de JavaScript se pueden llamar en una variedad de tipos y recuentos de argumentos.
Por ejemplo, podrías escribir una función para producir un Date
que tome un timestamp (un argumento) o una especificación de mes/día/año (tres argumentos).
En TypeScript, podemos especificar una función que se puede llamar de diferentes maneras escribiendo firmas de sobrecarga. Para hacer esto, escribe una cierta cantidad de firmas de función (generalmente dos o más), seguidas del cuerpo de la función:
En este ejemplo, escribimos dos sobrecargas: una que acepta un argumento y otra que acepta tres argumentos. Estas dos primeras firmas se denominan firmas de sobrecarga.
Luego, escribimos una implementación de función con una firma compatible. Las funciones tienen una firma de implementación, pero esta firma no se puede llamar directamente. Aunque escribimos una función con dos parámetros opcionales después del requerido, ¡no se puede llamar con dos parámetros!
Sobrecarga de firmas y firma de implementación
Esta es una fuente común de confusión. A menudo la gente escribe código como este y no entiende por qué hay un error:
Nuevamente, la firma utilizada para escribir el cuerpo de la función no se puede “ver” desde afuera.
La firma de la implementación no es visible desde el exterior. Al escribir una función sobrecargada, siempre debe tener dos o más firmas encima de la implementación de la función.
La firma de implementación también debe ser compatible con las firmas de sobrecarga. Por ejemplo, estas funciones tienen errores porque la firma de implementación no coincide con las sobrecargas de manera correcta:
Escribir buenas sobrecargas
Al igual que los generics, hay algunas pautas que debes seguir al usar sobrecargas de funciones. Seguir estos principios hará que tu función sea más fácil de llamar, de entender y de implementar.
Consideremos una función que devuelve la longitud de una cadena o un array:
Esta función está bien; podemos invocarlo con cadenas o arrays. Sin embargo, no podemos invocarlo con un valor que podría ser una cadena o un array, porque TypeScript solo puede resolver una llamada de función a una única sobrecarga:
Debido a que ambas sobrecargas tienen el mismo número de argumentos y el mismo tipo de retorno, podemos escribir una versión no sobrecargada de la función:
¡Esto es mucho mejor! Los callers pueden invocar esto con cualquier tipo de valor y, como beneficio adicional, no tenemos que encontrar una firma de implementación correcta.
Prefiere siempre parámetros con tipos de unión en lugar de sobrecargas cuando sea posible
Declarando this
en una Función
TypeScript inferirá cuál debería ser this
en una función mediante el análisis de flujo de código, por ejemplo en lo siguiente:
TypeScript entiende que la función user.becomeAdmin
tiene un this
correspondiente, que es el objeto externo user
. this
, puede ser suficiente para muchos casos, pero hay muchos casos en los que necesitas más control sobre qué objeto representa this
. La especificación de JavaScript establece que no puedes tener un parámetro llamado this
, por lo que TypeScript usa ese espacio de sintaxis para permitirte declarar el tipo de this
en el cuerpo de la función.
Este patrón es común con las API de estilo callback, donde normalmente otro objeto controla cuándo se llama a tu función. Ten en cuenta que necesitas usar function
y no funciones de flecha para obtener este comportamiento:
Otros tipos que debes conocer
Hay algunos tipos adicionales que querrás reconocer y que aparecen con frecuencia cuando trabajas con tipos de funciones. Como todos los tipos, puedes usarlos en todas partes, pero son especialmente relevantes en el contexto de las funciones.
El tipo void
El tipo void
representa el valor de retorno de funciones que no devuelven un valor.
Es el tipo inferido cada vez que una función no tiene declaraciones return
o no devuelve ningún valor explícito de esas declaraciones de retorno:
En JavaScript, una función que no devuelve ningún valor devolverá implícitamente el valor undefined
.
Sin embargo, void
y undefined
no son lo mismo en TypeScript.
Hay más detalles al final de este capítulo.
void
no es lo mismo queundefined
.
El tipo object
El tipo especial object
se refiere a cualquier valor que no sea primitivo (string
, number
, bigint
, boolean
, symbol
, null
, o undefined
).
Esto es diferente del tipo de objeto vacío { }
, y también diferente del tipo global Object
.
Es muy probable que nunca utilices Object
.
object
no esObject
. ¡Utiliza siempreobject
!
Ten en cuenta que en JavaScript, los valores de las funciones son objetos: tienen propiedades, tienen Object.prototype
en su cadena de prototipo, son instanceof Object
, puedes llamar a Object.keys
en ellos, etcétera.
Por esta razón, los tipos de funciones se consideran object
s en TypeScript.
El tipo unknown
El tipo unknown
representa cualquier valor.
Esto es similar al tipo any
, pero es más seguro porque no es legal hacer nada con un valor unknown
:
Esto es útil al describir tipos de funciones porque puedes describir funciones que aceptan cualquier valor sin tener ningún valor any
en el cuerpo de tu función.
A la inversa, puedes describir una función que devuelve un valor de tipo unknown
:
El tipo never
Algunas funciones nunca devuelven un valor:
El tipo never
representa valores que nunca se observan.
En un tipo de retorno, esto significa que la función genera una excepción o finaliza la ejecución del programa.
never
también aparece cuando TypeScript determina que no queda nada en una unión.
El tipo Function
El tipo global Function
describe propiedades como bind
, call
, apply
y otras presentes en todos los valores de funciones en JavaScript.
También tiene la propiedad especial de que siempre se pueden llamar valores de tipo Function
; estas llamadas devuelven any
:
Esta es una llamada a función sin tipo y generalmente es mejor evitarla debido al tipo de retorno inseguro any
.
Si necesitas aceptar una función arbitraria pero no tienes intención de llamarla, el tipo () => void
es generalmente más seguro.
Parámetros y argumentos Rest
Lectura en segundo plano: Rest Parameters ↗ Syntax difundido ↗
Parámetros rest
Además de usar parámetros opcionales o sobrecargas para crear funciones que puedan aceptar una cantidad de argumentos fijos, también podemos definir funciones que toman un número ilimitado de argumentos usando parámetros rest.
Un parámetro rest aparece después de todos los demás parámetros y usa la sintaxis ...
:
En TypeScript, la anotación de tipo en estos parámetros es implícitamente any[]
en lugar de any
, y cualquier anotación de tipo proporcionada debe tener la forma Array<T>
o T []
, o un tipo de tupla (del que aprenderemos más adelante).
Argumentos Rest
A la inversa, podemos proporcionar un número variable de argumentos de un objeto iterable (por ejemplo, un array) usando la sintaxis spread.
Por ejemplo, el método push
de arrays toma cualquier número de argumentos:
Ten en cuenta que, en general, TypeScript no asume que los arrays sean inmutables. Esto puede llevar a algunos comportamientos sorprendentes:
La mejor solución para esta situación depende un poco de tu código, pero en general un contexto const
es la solución más sencilla:
El uso de argumentos rest puede requerir activar downlevelIteration
↗ cuando apuntas a runtimes más antiguos.
Desestructuración de parámetros
Lectura previa: Asignación de desestructuración ↗
Puedes usar la desestructuración de parámetros para descomprimir convenientemente objetos proporcionados como argumento en una o más variables locales en el cuerpo de la función. En JavaScript, se ve así:
La anotación de tipo para el objeto va después de la sintaxis de desestructuración:
Esto puede parecer un poco verboso, pero aquí también puedes usar un tipo con nombre:
Asignabilidad de Funciones
Tipo de retorno void
El tipo de retorno void
para funciones puede producir un comportamiento inusual pero esperado.
El tipado contextual con un tipo de retorno void
no obliga a las funciones a no devolver algo. Otra forma de decir esto es un tipo de función contextual con un tipo de retorno void
(type voidFunc = () => void
), cuando se implementa, puede devolver cualquier otro valor, pero será ignorado.
Así, las siguientes implementaciones del tipo () => void
son válidas:
Y cuando el valor de retorno de una de estas funciones se asigna a otra variable, conservará el tipo de void
:
Este comportamiento existe para que el siguiente código sea válido aunque Array.prototype.push
devuelva un número y el método Array.prototype.forEach
espere una función con un tipo de retorno de void
.
Hay otro caso especial que debes tener en cuenta: cuando la definición de una función literal tiene un tipo de retorno void
, esa función no debe devolver nada.
Para obtener más información sobre void
, consulta estas otras entradas de documentación: