Capítulo 4. Diseño tipográfico
Este trabajo se ha traducido utilizando IA. Agradecemos tus opiniones y comentarios: translation-feedback@oreilly.com
Enséñame tus organigramas y oculta tus tablas, y seguiré desconcertado. Enséñame tus tablas, y normalmente no necesitaré tus organigramas; serán obvios.
Fred Brooks, Mes del Hombre Mítico
El lenguaje de la cita de Fred Brooks es anticuado, pero el sentimiento sigue siendo cierto: el código es difícil de entender si no puedes ver los datos o tipos de datos sobre los que opera. Esta es una de las grandes ventajas de un sistema de tipos: al escribir los tipos, los haces visibles para los lectores de tu código. Y esto hace que tu código sea comprensible.
Otros capítulos cubren los entresijos de los tipos TypeScript: cómo utilizarlos, cómo inferirlos y cómo escribir declaraciones con ellos. Este capítulo trata del diseño de los propios tipos. Todos los ejemplos de este capítulo están escritos pensando en TypeScript, pero la mayoría de las ideas son de aplicación más general.
Si escribes bien tus tipos, con un poco de suerte tus diagramas de flujo también serán obvios.
Tema 28: Prefiere tipos que siempre representen estados válidos
Si diseñas bien tus tipos, tu código debería ser sencillo de escribir. Pero si diseñas mal tus tipos, no te salvará ningún tipo de ingenio o documentación. Tu código será confuso y propenso a los errores.
Una clave para un diseño tipográfico eficaz es crear tipos que sólo puedan representar un estado válido. Este artículo recorre algunos ejemplos de cómo esto puede salir mal y te muestra cómo solucionarlo.
Supón que estás construyendo una aplicación web que te permite seleccionar una página, cargar el contenido de esa página y, a continuación, mostrarlo. Podrías escribir el estado así
interface
State
{
pageText
:string
;
isLoading
:boolean
;
error?
:string
;
}
Cuando escribas el código para representar la página, deberás tener en cuenta todos estos campos:
function
renderPage
(
state
:State
)
{
if
(
state
.
error
)
{
return
`Error! Unable to load
${
currentPage
}
:
${
state
.
error
}
`
;
}
else
if
(
state
.
isLoading
)
{
return
`Loading
${
currentPage
}
...`
;
}
return
`<h1>
${
currentPage
}
</h1>
\
n
${
state
.
pageText
}
`
;
}
¿Pero esto es correcto? ¿Y si isLoading
y error
están ambos fijados? ¿Qué significaría eso? ¿Es mejor mostrar el mensaje de carga o el mensaje de error? Es difícil decirlo. No hay suficiente información disponible.
¿Y si estás escribiendo una función changePage
? Aquí tienes un intento:
async
function
changePage
(
state
:State
,
newPage
:string
)
{
state
.
isLoading
=
true
;
try
{
const
response
=
await
fetch
(
getUrlForPage
(
newPage
));
if
(
!
response
.
ok
)
{
throw
new
Error
(
`Unable to load
${
newPage
}
:
${
response
.
statusText
}
`
);
}
const
text
=
await
response
.
text
();
state
.
isLoading
=
false
;
state
.
pageText
=
text
;
}
catch
(
e
)
{
state
.
error
=
''
+
e
;
}
}
¡Hay muchos problemas con esto! He aquí algunos:
-
Olvidamos poner
state.isLoading
enfalse
en el caso de error. -
No hemos borrado
state.error
, por lo que si la solicitud anterior falló, seguirás viendo ese mensaje de error en lugar de un mensaje de carga. -
Si el usuario vuelve a cambiar de página mientras ésta se está cargando, quién sabe lo que ocurrirá. Puede que vea una página nueva y luego un error, o la primera página y no la segunda, dependiendo del orden en que vuelvan las respuestas.
El problema es que el estado incluye muy poca información (¿qué petición ha fallado? ¿cuál se está cargando?) y demasiada: el tipo State
permite establecer tanto isLoading
como error
, aunque esto represente un estado no válido. Esto hace que tanto render()
y changePage()
imposibles de implementar bien.
Aquí tienes una forma mejor de representar el estado de la aplicación:
interface
RequestPending
{
state
:
'pending'
;
}
interface
RequestError
{
state
:
'error'
;
error
:string
;
}
interface
RequestSuccess
{
state
:
'ok'
;
pageText
:string
;
}
type
RequestState
=
RequestPending
|
RequestError
|
RequestSuccess
;
interface
State
{
currentPage
:string
;
requests
:
{[
page
:string
]
:
RequestState
};
}
Utiliza una unión etiquetada (también conocida como "unión discriminada") para modelar explícitamente los distintos estados en que puede encontrarse una solicitud de red. Esta versión del estado es de tres a cuatro veces más larga, pero tiene la enorme ventaja de no admitir estados no válidos. La página actual se modela explícitamente, al igual que el estado de cada solicitud que emitas. Como resultado, las funciones renderPage
y changePage
son fáciles de implementar:
function
renderPage
(
state
:State
)
{
const
{
currentPage
}
=
state
;
const
requestState
=
state
.
requests
[
currentPage
];
switch
(
requestState
.
state
)
{
case
'pending'
:
return
`Loading
${
currentPage
}
...`
;
case
'error'
:
return
`Error! Unable to load
${
currentPage
}
:
${
requestState
.
error
}
`
;
case
'ok'
:
return
`<h1>
${
currentPage
}
</h1>
\
n
${
requestState
.
pageText
}
`
;
}
}
async
function
changePage
(
state
:State
,
newPage
:string
)
{
state
.
requests
[
newPage
]
=
{
state
:
'pending'
};
state
.
currentPage
=
newPage
;
try
{
const
response
=
await
fetch
(
getUrlForPage
(
newPage
));
if
(
!
response
.
ok
)
{
throw
new
Error
(
`Unable to load
${
newPage
}
:
${
response
.
statusText
}
`
);
}
const
pageText
=
await
response
.
text
();
state
.
requests
[
newPage
]
=
{
state
:
'ok'
,
pageText
};
}
catch
(
e
)
{
state
.
requests
[
newPage
]
=
{
state
:
'error'
,
error
:
''
+
e
};
}
}
La ambigüedad de la primera implementación ha desaparecido por completo: está claro cuál es la página actual, y cada solicitud se encuentra exactamente en un estado. Si el usuario cambia de página después de emitir una solicitud, tampoco hay problema. La antigua solicitud sigue completándose, pero no afecta a la interfaz de usuario.
Para un ejemplo más sencillo pero más terrible, considera el destino del vuelo 447 de Air France, un Airbus 330 que desapareció sobre el Atlántico el 1 de junio de 2009. El Airbus era un avión fly-by-wire, lo que significa que las entradas de control de los pilotos pasaban por un sistema informático antes de afectar a las superficies físicas de control del avión. Tras el accidente se plantearon muchas cuestiones sobre la conveniencia de confiar en los ordenadores para tomar decisiones de vida o muerte. Dos años después, cuando se recuperaron las grabadoras de la caja negra, revelaron muchos factores que condujeron al accidente. Pero uno de los principales fue el mal diseño del estado.
La cabina del Airbus 330 tenía un conjunto separado de mandos para el piloto y el copiloto. Los "side sticks" controlaban el ángulo de ataque. Tirando hacia atrás, el avión ascendía, mientras que empujando hacia delante caía en picado. El Airbus 330 utilizaba un sistema llamado modo de "doble entrada", que permitía que los dos sticks laterales se movieran independientemente. He aquí cómo podrías modelar su estado en TypeScript:
interface
CockpitControls
{
/** Angle of the left side stick in degrees, 0 = neutral, + = forward */
leftSideStick
:number
;
/** Angle of the right side stick in degrees, 0 = neutral, + = forward */
rightSideStick
:number
;
}
Supongamos que te dan esta estructura de datos y te piden que escribas una función getStickSetting
que calcule el ajuste actual de la palanca. ¿Cómo lo harías?
Una forma sería asumir que el piloto (que se sienta a la izquierda) tiene el control:
function
getStickSetting
(
controls
:CockpitControls
)
{
return
controls
.
leftSideStick
;
}
Pero, ¿y si el copiloto ha tomado el control? Tal vez debas utilizar el stick que esté más alejado de cero:
function
getStickSetting
(
controls
:CockpitControls
)
{
const
{
leftSideStick
,
rightSideStick
}
=
controls
;
if
(
leftSideStick
===
0
)
{
return
rightSideStick
;
}
return
leftSideStick
;
}
Pero hay un problema con esta implementación: sólo podemos estar seguros de devolver el ajuste izquierdo si el derecho es neutro. Así que deberías comprobarlo:
function
getStickSetting
(
controls
:CockpitControls
)
{
const
{
leftSideStick
,
rightSideStick
}
=
controls
;
if
(
leftSideStick
===
0
)
{
return
rightSideStick
;
}
else
if
(
rightSideStick
===
0
)
{
return
leftSideStick
;
}
// ???
}
¿Qué haces si ambos son distintos de cero? Esperemos que sean más o menos iguales, en cuyo caso podrías simplemente promediarlos:
function
getStickSetting
(
controls
:CockpitControls
)
{
const
{
leftSideStick
,
rightSideStick
}
=
controls
;
if
(
leftSideStick
===
0
)
{
return
rightSideStick
;
}
else
if
(
rightSideStick
===
0
)
{
return
leftSideStick
;
}
if
(
Math
.
abs
(
leftSideStick
-
rightSideStick
)
<
5
)
{
return
(
leftSideStick
+
rightSideStick
)
/
2
;
}
// ???
}
Pero, ¿y si no lo son? ¿Puedes lanzar un error? En realidad no: ¡los alerones tienen que estar colocados en algún ángulo!
En el Air France 447, el copiloto tiró silenciosamente hacia atrás del mando lateral cuando el avión entró en tormenta. Ganó altitud, pero acabó perdiendo velocidad y entró en pérdida, una situación en la que el avión se mueve demasiado despacio para generar sustentación de forma eficaz. Empezó a descender.
Para escapar de una entrada en pérdida, los pilotos están entrenados para empujar los mandos hacia delante para hacer que el avión caiga en picado y recupere velocidad. Esto es exactamente lo que hizo el piloto. Pero el copiloto seguía tirando silenciosamente hacia atrás de su mando lateral. Y la función del Airbus tenía este aspecto:
function
getStickSetting
(
controls
:CockpitControls
)
{
return
(
controls
.
leftSideStick
+
controls
.
rightSideStick
)
/
2
;
}
A pesar de que el piloto empujó la palanca completamente hacia delante, la media se quedó en nada. No tenía ni idea de por qué el avión no se sumergía. Cuando el copiloto reveló lo que había hecho, el avión había perdido demasiada altitud para recuperarse y se estrelló en el océano, matando a las 228 personas que iban a bordo.
La cuestión de todo esto es que ¡no hay una buena forma de implementar getStickSetting
dada esa entrada! La función se ha configurado para que falle. En la mayoría de los aviones, los dos conjuntos de mandos están conectados mecánicamente. Si el copiloto tira hacia atrás, los mandos del piloto también lo harán. El estado de estos mandos es sencillo de expresar:
interface
CockpitControls
{
/** Angle of the stick in degrees, 0 = neutral, + = forward */
stickAngle
:number
;
}
Y ahora, como en la cita de Fred Brooks del principio del capítulo, nuestros diagramas de flujo son obvios. No necesitas en absoluto una función getStickSetting
.
Cuando diseñes tus tipos, ten en cuenta qué valores incluyes y cuáles excluyes. Si sólo permites valores que representen estados válidos, tu código será más fácil de escribir y a TypeScript le resultará más fácil comprobarlo. Éste es un principio muy general, y varios de los otros puntos de este capítulo cubrirán manifestaciones específicas del mismo.
Tema 29: Sé liberal en lo que aceptas y estricto en lo que produces
Esta idea de se conoce como principio de robustez o Ley de Postel, en honor a Jon Postel, que la escribió en el contexto del TCP:
Las implementaciones TCP deben seguir un principio general de robustez: sé conservador en lo que haces, sé liberal en lo que aceptas de los demás.
Una regla similar se aplica a los contratos de las funciones. Está bien que tus funciones sean amplias en lo que aceptan como entradas, pero en general deben ser más específicas en lo que producen como salidas.
Por ejemplo, una API de mapeado 3D puede proporcionar un modo de posicionar la cámara y calcular una ventana gráfica para un cuadro delimitador:
declare
function
setCamera
(
camera
:CameraOptions
)
:
void
;
declare
function
viewportForBounds
(
bounds
:LngLatBounds
)
:
CameraOptions
;
Es conveniente que el resultado de viewportForBounds
se pueda pasar directamente a setCamera
para posicionar la cámara.
Veamos las definiciones de estos tipos:
interface
CameraOptions
{
center?
:LngLat
;
zoom?
:number
;
bearing?
:number
;
pitch?
:number
;
}
type
LngLat
=
{
lng
:number
;
lat
:number
;
}
|
{
lon
:number
;
lat
:number
;
}
|
[
number
,
number
];
Los campos de CameraOptions
son todos opcionales porque puede que quieras establecer sólo el centro o el zoom sin cambiar el rumbo o el cabeceo. El tipo LngLat
también hace que setCamera
sea liberal en cuanto a lo que acepta: puedes pasar un objeto {lng, lat}
, un objeto {lon, lat}
o un par [lng, lat]
si estás seguro de que tienes el orden correcto. Estas adaptaciones hacen que la función sea fácil de llamar.
La función viewportForBounds
recoge otro tipo "liberal":
type
LngLatBounds
=
{
northeast
:LngLat
,
southwest
:LngLat
}
|
[
LngLat
,
LngLat
]
|
[
number
,
number
,
number
,
number
];
Puedes especificar los límites utilizando esquinas con nombre, un par de lat/lngs, o una cuádruple tupla si estás seguro de haber acertado con el orden. Como LngLat
ya admite tres formas, hay nada menos que 19 formas posibles para LngLatBounds
. ¡Realmente liberal!
Ahora escribamos una función que ajuste la ventana gráfica para acomodar una Característica GeoJSON y almacene la nueva ventana gráfica en la URL (para una definición de calculateBoundingBox
, consulta el Tema 31):
function
focusOnFeature
(
f
:Feature
)
{
const
bounds
=
calculateBoundingBox
(
f
);
const
camera
=
viewportForBounds
(
bounds
);
setCamera
(
camera
);
const
{
center
:
{
lat
,
lng
},
zoom
}
=
camera
;
// ~~~ Property 'lat' does not exist on type ...
// ~~~ Property 'lng' does not exist on type ...
zoom
;
// Type is number | undefined
window
.
location
.
search
=
`?v=@
${
lat
}
,
${
lng
}
z
${
zoom
}
`
;
}
¡Vaya! Sólo existe la propiedad zoom
, pero su tipo se infiere como number|undefined
, lo que también es problemático. La cuestión es que la declaración de tipo de viewportForBounds
indica que es liberal no sólo en lo que acepta, sino también en lo que produce. La única forma segura de utilizar el resultado camera
es introducir una rama de código para cada componente del tipo de unión(Tema 22).
El tipo de retorno con muchas propiedades opcionales y tipos de unión hace que viewportForBounds
sea difícil de utilizar. Su amplio tipo de parámetro es conveniente, pero su amplio tipo de retorno no lo es. Una API más cómoda sería estricta en lo que produce.
Una forma de hacerlo es distinguir un formato canónico para las coordenadas. Siguiendo la convención de JavaScript de distinguir entre "Array" y "Array-like"(Tema 16), puedes establecer una distinción entre LngLat
y LngLatLike
. También puedes distinguir entre un tipo Camera
completamente definido y la versión parcial aceptada por setCamera
:
interface
LngLat
{
lng
:number
;
lat
:number
;
};
type
LngLatLike
=
LngLat
|
{
lon
:number
;
lat
:number
;
}
|
[
number
,
number
];
interface
Camera
{
center
:LngLat
;
zoom
:number
;
bearing
:number
;
pitch
:number
;
}
interface
CameraOptions
extends
Omit
<
Partial
<
Camera
>
,
'center'
>
{
center?
:LngLatLike
;
}
type
LngLatBounds
=
{
northeast
:LngLatLike
,
southwest
:LngLatLike
}
|
[
LngLatLike
,
LngLatLike
]
|
[
number
,
number
,
number
,
number
];
declare
function
setCamera
(
camera
:CameraOptions
)
:
void
;
declare
function
viewportForBounds
(
bounds
:LngLatBounds
)
:
Camera
;
El tipo suelto CameraOptions
adapta el tipo más estricto Camera
(Elemento 14).
Utilizar Partial<Camera>
como tipo de parámetro en setCamera
no funcionaría aquí, ya que quieres permitir objetos LngLatLike
para la propiedad center
. Y no puedes escribir "CameraOptions extends Partial<Camera>
" ya que LngLatLike
es un superconjunto de LngLat
, no un subconjunto(Punto 7). Si esto te parece demasiado complicado, también podrías escribir el tipo explícitamente a costa de alguna repetición:
interface
CameraOptions
{
center?
:LngLatLike
;
zoom?
:number
;
bearing?
:number
;
pitch?
:number
;
}
En cualquier caso, con estas nuevas declaraciones de tipo, la función focusOnFeature
pasa el comprobador de tipos:
function
focusOnFeature
(
f
:Feature
)
{
const
bounds
=
calculateBoundingBox
(
f
);
const
camera
=
viewportForBounds
(
bounds
);
setCamera
(
camera
);
const
{
center
:
{
lat
,
lng
},
zoom
}
=
camera
;
// OK
zoom
;
// Type is number
window
.
location
.
search
=
`?v=@
${
lat
}
,
${
lng
}
z
${
zoom
}
`
;
}
Esta vez el tipo de zoom
es number
, en lugar de number|undefined
. La función viewportForBounds
es ahora mucho más fácil de utilizar. Si hubiera otras funciones que produjeran límites, también tendrías que introducir una forma canónica y una distinción entre LngLatBounds
y LngLatBoundsLike
.
¿Permitir 19 formas posibles de cuadro delimitador es un buen diseño? Tal vez no. Pero si estás escribiendo declaraciones de tipos para una biblioteca que hace esto, necesitas modelar su comportamiento. Eso sí, ¡no tengas 19 tipos de retorno!
Cosas para recordar
-
Los tipos de entrada suelen ser más amplios que los de salida. Las propiedades opcionales y los tipos de unión son más comunes en los tipos de parámetros que en los tipos de retorno.
-
Para reutilizar tipos entre parámetros y tipos de retorno, introduce una forma canónica (para los tipos de retorno) y una forma más laxa (para los parámetros).
Tema 30: No repitas la información del tipo en la documentación
¿Qué hay de malo en este código en?
/**
* Returns a string with the foreground color.
* Takes zero or one arguments. With no arguments, returns the
* standard foreground color. With one argument, returns the foreground color
* for a particular page.
*/
function
getForegroundColor
(
page?
:string
)
{
return
page
===
'login'
?
{
r
:127
,
g
:127
,
b
:127
}
:
{
r
:0
,
g
:0
,
b
:0
};
}
¡El código y el comentario no están de acuerdo! Sin más contexto es difícil decir cuál es el correcto, pero está claro que algo falla. Como solía decir un profesor mío, "cuando tu código y tus comentarios discrepan, ¡ambos están mal!".
Supongamos que el código representa el comportamiento deseado. Hay algunos problemas con este comentario:
-
Dice que la función devuelve el color como
string
cuando en realidad devuelve un objeto{r, g, b}
. -
Explica que la función toma cero o uno argumentos, lo que ya queda claro en la firma del tipo.
-
Es innecesariamente prolijo: ¡el comentario es más largo que la declaración de la función y su implementación!
El sistema de anotación de tipos de TypeScript está diseñado para ser compacto, descriptivo y legible. Sus desarrolladores son expertos en el lenguaje con décadas de experiencia. ¡Es casi seguro que es una forma mejor de expresar los tipos de las entradas y salidas de tus funciones que tu prosa!
Y como el compilador de TypeScript comprueba tus anotaciones de tipo, nunca se desincronizarán con la implementación. Quizá getForegroundColor
devolvía una cadena, pero luego se cambió para que devolviera un objeto. La persona que hizo el cambio podría haber olvidado actualizar el comentario largo.
Nada permanece sincronizado a menos que se le fuerce a ello. Con las anotaciones de tipo, ¡el verificador de tipos de TypeScript es esa fuerza! Si pones la información de tipos en las anotaciones y no en la documentación, aumentas enormemente tu confianza en que seguirá siendo correcta a medida que evolucione el código.
Un comentario mejor podría ser el siguiente
/** Get the foreground color for the application or a specific page. */
function
getForegroundColor
(
page?
:string
)
:
Color
{
// ...
}
Si quieres describir un parámetro concreto, utiliza una anotación JSDoc @param
. Consulta el Tema 48 para saber más sobre esto.
Los comentarios sobre la falta de mutación también son sospechosos. No te limites a decir que no modificas un parámetro:
/** Does not modify nums */
function
sort
(
nums
:number
[])
{
/* ... */
}
En su lugar, decláralo readonly
(punto 17) y deja que TypeScript haga cumplir el contrato:
function
sort
(
nums
:readonly
number
[])
{
/* ... */
}
Lo que es válido para los comentarios en también lo es para los nombres de las variables. Evita poner tipos en ellos: en lugar de nombrar una variable ageNum
, nómbrala age
y asegúrate de que es realmente una number
.
Una excepción son los números con unidades. Si no está claro cuáles son las unidades, puedes incluirlas en el nombre de una variable o propiedad. Por ejemplo, timeMs
es un nombre mucho más claro que simplemente time
, y temperatureC
es un nombre mucho más claro que temperature
. El punto 37 describe las "marcas", que proporcionan un enfoque más seguro para modelar unidades.
Cosas para recordar
-
Evita repetir información sobre tipos en comentarios y nombres de variables. En el mejor de los casos duplica las declaraciones de tipo, y en el peor dará lugar a información contradictoria.
-
Considera la posibilidad de incluir unidades en los nombres de las variables si no quedan claras en el tipo (por ejemplo,
timeMs
otemperatureC
).
Tema 31: Empuja los valores nulos al perímetro de tus tipos
Cuando activas por primera vez strictNullChecks
, puede parecer que tienes que añadir decenas de sentencias if comprobando los valores null
y undefined
por todo tu código. Esto suele deberse a que las relaciones entre valores nulos y no nulos son implícitas: cuando la variable A es no nula, sabes que la variable B también lo es y viceversa. Estas relaciones implícitas son confusas tanto para los lectores humanos de tu código como para el comprobador de tipos.
Es más fácil trabajar con los valores cuando son completamente nulos o completamente no nulos, en lugar de una mezcla. Puedes modelar esto empujando los valores nulos hacia el perímetro de tus estructuras.
Supongamos que quieres calcular el mínimo y el máximo de una lista de números. Llamaremos a esto "extensión". Aquí tienes un intento:
function
extent
(
nums
:number
[])
{
let
min
,
max
;
for
(
const
num
of
nums
)
{
if
(
!
min
)
{
min
=
num
;
max
=
num
;
}
else
{
min
=
Math
.
min
(
min
,
num
);
max
=
Math
.
max
(
max
,
num
);
}
}
return
[
min
,
max
];
}
El código comprueba el tipo (sin strictNullChecks
) y tiene un tipo de retorno inferido de number[]
, lo que parece correcto. Pero tiene un error y un fallo de diseño:
-
Si el mín. o máx. es cero, puede que se anule. Por ejemplo,
extent([0, 1, 2])
devolverá[1, 2]
en lugar de[0, 2]
. -
Si la matriz
nums
está vacía, la función devolverá[undefined, undefined]
. Este tipo de objeto con variosundefined
s será difícil de trabajar para los clientes y es exactamente la clase de tipo que este artículo desaconseja. Sabemos por la lectura del código fuente quemin
ymax
serán ambosundefined
o ninguno de los dos, pero esa información no está representada en el sistema de tipos.
Si activas strictNullChecks
, ambos problemas se hacen más evidentes:
function
extent
(
nums
:number
[])
{
let
min
,
max
;
for
(
const
num
of
nums
)
{
if
(
!
min
)
{
min
=
num
;
max
=
num
;
}
else
{
min
=
Math
.
min
(
min
,
num
);
max
=
Math
.
max
(
max
,
num
);
// ~~~ Argument of type 'number | undefined' is not
// assignable to parameter of type 'number'
}
}
return
[
min
,
max
];
}
El tipo de retorno de extent
se infiere ahora como (number | undefined)[]
, lo que hace más evidente el fallo de diseño. Es probable que esto se manifieste como un error de tipo siempre que llames a extent
:
const
[
min
,
max
]
=
extent
([
0
,
1
,
2
]);
const
span
=
max
-
min
;
// ~~~ ~~~ Object is possibly 'undefined'
El error en la implementación de extent
se produce porque has excluido undefined
como valor para min
pero no para max
. Los dos se inicializan juntos, pero esta información no está presente en el sistema de tipos. Podrías hacerlo desaparecer añadiendo también una comprobación para max
, pero esto sería duplicar el error.
Una solución mejor es poner el mínimo y el máximo en el mismo objeto y hacer que este objeto sea totalmente null
o totalmente nonull
:
function
extent
(
nums
:number
[])
{
let
result
:
[
number
,
number
]
|
null
=
null
;
for
(
const
num
of
nums
)
{
if
(
!
result
)
{
result
=
[
num
,
num
];
}
else
{
result
=
[
Math
.
min
(
num
,
result
[
0
]),
Math
.
max
(
num
,
result
[
1
])];
}
}
return
result
;
}
El tipo de retorno es ahora [number, number] | null
, con el que es más fácil trabajar para los clientes. Los valores mínimo y máximo pueden recuperarse con una aserción no nula:
const
[
min
,
max
]
=
extent
([
0
,
1
,
2
])
!
;
const
span
=
max
-
min
;
// OK
o un solo cheque:
const
range
=
extent
([
0
,
1
,
2
]);
if
(
range
)
{
const
[
min
,
max
]
=
range
;
const
span
=
max
-
min
;
// OK
}
Al utilizar un único objeto para rastrear la extensión, hemos mejorado nuestro diseño, hemos ayudado a TypeScript a comprender la relación entre valores nulos y hemos corregido el error: la comprobación de if (!result)
ya no da problemas.
Una mezcla de valores nulos y no nulos también puede provocar problemas en las clases. Por ejemplo, supongamos que tienes una clase que representa tanto a un usuario como a sus mensajes en un foro:
class
UserPosts
{
user
:UserInfo
|
null
;
posts
:Post
[]
|
null
;
constructor
()
{
this
.
user
=
null
;
this
.
posts
=
null
;
}
async
init
(
userId
:string
)
{
return
Promise
.
all
([
async
()
=>
this
.
user
=
await
fetchUser
(
userId
),
async
()
=>
this
.
posts
=
await
fetchPostsForUser
(
userId
)
]);
}
getUserName() {
// ...?
}
}
Mientras se cargan las dos solicitudes de red, las propiedades user
y posts
serán null
. En cualquier momento, ambas pueden ser null
, una puede ser null
, o ambas pueden no sernull
. Existen cuatro posibilidades. Esta complejidad se filtrará en todos los métodos de la clase. Es casi seguro que este diseño provocará confusión, una proliferación de comprobaciones null
y errores.
Un diseño mejor esperaría hasta que todos los datos utilizados por la clase estuvieran disponibles:
class
UserPosts
{
user
:UserInfo
;
posts
:Post
[];
constructor
(
user
:UserInfo
,
posts
:Post
[])
{
this
.
user
=
user
;
this
.
posts
=
posts
;
}
static
async
init
(
userId
:string
)
:
Promise
<
UserPosts
>
{
const
[
user
,
posts
]
=
await
Promise
.
all
([
fetchUser
(
userId
),
fetchPostsForUser
(
userId
)
]);
return
new
UserPosts
(
user
,
posts
);
}
getUserName() {
return
this
.
user
.
name
;
}
}
Ahora la clase UserPosts
es totalmente nonull
, y es fácil escribir métodos correctos en ella. Por supuesto, si necesitas realizar operaciones mientras los datos están parcialmente cargados, entonces tendrás que lidiar con la multiplicidad de estados null
y nonull
.
(No caigas en la tentación de sustituir las propiedades anulables por Promesas. Esto suele conducir a un código aún más confuso y obliga a que todos tus métodos sean asíncronos. Las promesas aclaran el código que carga datos, pero tienden a tener el efecto contrario en la clase que utiliza esos datos).
Cosas para recordar
-
Evita diseños en los que un valor que sea
null
o nonull
esté implícitamente relacionado con otro valor que seanull
o nonull
. -
Empuja los valores
null
hacia el perímetro de tu API haciendo que los objetos más grandes seannull
o totalmente nonull
. Esto hará que el código sea más claro tanto para los lectores humanos como para el comprobador de tipos. -
Considera la posibilidad de crear una clase totalmente no
null
y construirla cuando todos los valores estén disponibles. -
Aunque
strictNullChecks
puede señalar muchos problemas en tu código, es indispensable para sacar a la luz el comportamiento de las funciones con respecto a los valores nulos.
Tema 32: Prefiere Uniones de Interfaces a Interfaces de Uniones
Si creas una interfaz cuyas propiedades son tipos de unión, debes preguntarte si el tipo tendría más sentido como unión de interfaces más precisas.
Supón que estás construyendo un programa de dibujo vectorial y quieres definir una interfaz para capas con tipos de geometría específicos:
interface
Layer
{
layout
:FillLayout
|
LineLayout
|
PointLayout
;
paint
:FillPaint
|
LinePaint
|
PointPaint
;
}
El campo layout
controla cómo y dónde se dibujan las formas (¿esquinas redondeadas? ¿rectas?), mientras que el campo paint
controla los estilos (¿la línea es azul? ¿gruesa? ¿fina? ¿punteada?).
¿Tendría sentido tener una capa cuyo layout
sea LineLayout
pero cuya propiedad paint
sea FillPaint
? Probablemente no. Permitir esta posibilidad hace que el uso de la biblioteca sea más propenso a errores y dificulta el trabajo con esta interfaz.
Una forma mejor de modelar esto es con interfaces separadas para cada tipo de capa:
interface
FillLayer
{
layout
:FillLayout
;
paint
:FillPaint
;
}
interface
LineLayer
{
layout
:LineLayout
;
paint
:LinePaint
;
}
interface
PointLayer
{
layout
:PointLayout
;
paint
:PointPaint
;
}
type
Layer
=
FillLayer
|
LineLayer
|
PointLayer
;
Al definir Layer
de esta forma, has excluido la posibilidad de que se mezclen las propiedades layout
y paint
. Éste es un ejemplo de cómo seguir el consejo del Tema28 de preferir tipos que sólo representen estados válidos.
El ejemplo más común de este patrón es la "unión etiquetada" (o "unión discriminada"). En este caso, una de las propiedades es una unión de tipos literales de cadena:
interface
Layer
{
type
:'fill'
|
'line'
|
'point'
;
layout
:FillLayout
|
LineLayout
|
PointLayout
;
paint
:FillPaint
|
LinePaint
|
PointPaint
;
}
Como antes, ¿tendría sentido tener type: 'fill'
pero luego un LineLayout
y PointPaint
? Desde luego que no. Convierte Layer
en una unión de interfaces para excluir esta posibilidad:
interface
FillLayer
{
type
:'fill'
;
layout
:FillLayout
;
paint
:FillPaint
;
}
interface
LineLayer
{
type
:'line'
;
layout
:LineLayout
;
paint
:LinePaint
;
}
interface
PointLayer
{
type
:'paint'
;
layout
:PointLayout
;
paint
:PointPaint
;
}
type
Layer
=
FillLayer
|
LineLayer
|
PointLayer
;
La propiedad type
es la "etiqueta" y puede utilizarse para determinar con qué tipo de Layer
estás trabajando en tiempo de ejecución. TypeScript también es capaz de acotar el tipo de Layer
basándose en la etiqueta:
function
drawLayer
(
layer
:Layer
)
{
if
(
layer
.
type
===
'fill'
)
{
const
{
paint
}
=
layer
;
// Type is FillPaint
const
{
layout
}
=
layer
;
// Type is FillLayout
}
else
if
(
layer
.
type
===
'line'
)
{
const
{
paint
}
=
layer
;
// Type is LinePaint
const
{
layout
}
=
layer
;
// Type is LineLayout
}
else
{
const
{
paint
}
=
layer
;
// Type is PointPaint
const
{
layout
}
=
layer
;
// Type is PointLayout
}
}
Al modelar correctamente la relación entre las propiedades de este tipo, ayudas a TypeScript a comprobar la corrección de tu código. El mismo código con la definición inicial de Layer
habría estado abarrotado de aserciones de tipo.
Debido a que funcionan tan bien con el verificador de tipos de TypeScript, las uniones etiquetadas son omnipresentes en el código TypeScript. Reconoce este patrón y aplícalo siempre que puedas. Si puedes representar un tipo de datos en TypeScript con una unión etiquetada, suele ser una buena idea hacerlo. Si piensas en los campos opcionales como una unión de su tipo y undefined
, entonces también se ajustan a este patrón. Considera este tipo:
interface
Person
{
name
:string
;
// These will either both be present or not be present
placeOfBirth?
:string
;
dateOfBirth?
:Date
;
}
El comentario con información sobre el tipo es una señal clara de que puede haber un problema(punto 30). Existe una relación entre los campos placeOfBirth
y dateOfBirth
de la que no has informado a TypeScript.
Una forma mejor de modelar esto es trasladar ambas propiedades a un único objeto. Esto es similar a trasladar los valores de null
al perímetro(Tema 31):
interface
Person
{
name
:string
;
birth
?:
{
place
:string
;
date
:Date
;
}
}
Ahora TypeScript se queja de los valores con un lugar pero sin fecha de nacimiento:
const
alanT
:Person
=
{
name
:
'Alan Turing'
,
birth
:
{
// ~~~~ Property 'date' is missing in type
// '{ place: string; }' but required in type
// '{ place: string; date: Date; }'
place
:
'London'
}
}
Además, una función que toma un objeto Person
sólo necesita hacer una única comprobación:
function
eulogize
(
p
:Person
)
{
console
.
log
(
p
.
name
);
const
{
birth
}
=
p
;
if
(
birth
)
{
console
.
log
(
`was born on
${
birth
.
date
}
in
${
birth
.
place
}
.`
);
}
}
Si la estructura del tipo está fuera de tu control (por ejemplo, procede de una API), aún puedes modelar la relación entre estos campos utilizando una ya familiar unión de interfaces:
interface
Name
{
name
:string
;
}
interface
PersonWithBirth
extends
Name
{
placeOfBirth
:string
;
dateOfBirth
:Date
;
}
type
Person
=
Name
|
PersonWithBirth
;
Ahora obtienes algunas de las mismas ventajas que con el objeto anidado:
function
eulogize
(
p
:Person
)
{
if
(
'placeOfBirth'
in
p
)
{
p
// Type is PersonWithBirth
const
{
dateOfBirth
}
=
p
// OK, type is Date
}
}
En ambos casos, la definición del tipo deja más clara la relación entre las propiedades.
Cosas para recordar
-
Las interfaces con múltiples propiedades que son tipos de unión suelen ser un error porque oscurecen las relaciones entre estas propiedades.
-
Las uniones de interfaces son más precisas y pueden ser comprendidas por TypeScript.
-
Considera añadir una "etiqueta" a tu estructura para facilitar el análisis del flujo de control de TypeScript. Al estar tan bien soportadas, las uniones etiquetadas son omnipresentes en el código TypeScript.
Tema 33: Prefiere alternativas más precisas a los tipos de cadena
El dominio del tipo string
es grande: "x"
y "y"
están en él, pero también lo está el texto completo de Moby Dick (empieza "Call me Ishmael…"
y tiene alrededor de 1,2 millones de caracteres). Cuando declares una variable del tipo string
, debes preguntarte si sería más apropiado un tipo más estrecho.
Supón que estás creando una colección de música y quieres definir un tipo para un álbum. Aquí tienes un intento:
interface
Album
{
artist
:string
;
title
:string
;
releaseDate
:string
;
// YYYY-MM-DD
recordingType
:string
;
// E.g., "live" or "studio"
}
La prevalencia de los tipos string
y la información sobre tipos en los comentarios (véase el punto 30) son fuertes indicios de que este interface
no es del todo correcto. He aquí lo que puede ir mal:
const
kindOfBlue
:Album
=
{
artist
:
'Miles Davis'
,
title
:
'Kind of Blue'
,
releaseDate
:
'August 17th, 1959'
,
// Oops!
recordingType
:
'Studio'
,
// Oops!
};
// OK
El campo releaseDate
tiene un formato incorrecto (según el comentario) y "Studio"
está en mayúsculas donde debería estar en minúsculas. Pero ambos valores son cadenas, por lo que este objeto es asignable a Album
y el verificador de tipos no se queja.
Estos tipos amplios de string
también pueden enmascarar errores de objetos válidos de Album
. Por ejemplo:
function
recordRelease
(
title
:string
,
date
:string
)
{
/* ... */
}
recordRelease
(
kindOfBlue
.
releaseDate
,
kindOfBlue
.
title
);
// OK, should be error
Los parámetros se invierten en la llamada a recordRelease
, pero ambos son cadenas, por lo que el comprobador de tipos no se queja. Debido a la prevalencia de los tipos string
, el código como éste a veces se denomina "tipado por cadenas".
¿Puedes hacer los tipos más estrechos para evitar este tipo de problemas? Aunque el texto completo de Moby Dick sería un pesado nombre de artista o título de álbum, al menos es plausible. Así que string
es apropiado para estos campos. Para el campo releaseDate
es mejor utilizar simplemente un objeto Date
y evitar problemas de formato. Por último, para el campo recordingType
, puedes definir un tipo de unión con sólo dos valores (también podrías utilizar un enum
, pero en general recomiendo evitarlos; véase el punto 53):
type
RecordingType
=
'studio'
|
'live'
;
interface
Album
{
artist
:string
;
title
:string
;
releaseDate
:Date
;
recordingType
:RecordingType
;
}
Con estos cambios, TypeScript puede realizar una comprobación más exhaustiva en busca de errores:
const
kindOfBlue
:Album
=
{
artist
:
'Miles Davis'
,
title
:
'Kind of Blue'
,
releaseDate
:new
Date
(
'1959-08-17'
),
recordingType
:
'Studio'
// ~~~~~~~~~~~~ Type '"Studio"' is not assignable to type 'RecordingType'
};
Este enfoque tiene ventajas, además de una comprobación más estricta. En primer lugar, definir explícitamente el tipo garantiza que su significado no se pierda al pasar de un tipo a otro. Si, por ejemplo, quisieras encontrar álbumes sólo de un determinado tipo de grabación, podrías definir una función como ésta:
function
getAlbumsOfType
(
recordingType
:string
)
:
Album
[]
{
// ...
}
¿Cómo sabe la persona que llama a esta función qué se espera que sea recordingType
? Es sólo un string
. El comentario que explica que es "studio"
o "live"
está oculto en la definición de Album
, donde el usuario podría no pensar en mirar.
En segundo lugar, definir explícitamente un tipo te permite adjuntarle documentación (ver Tema 48):
/** What type of environment was this recording made in? */
type
RecordingType
=
'live'
|
'studio'
;
Cuando cambias getAlbumsOfType
por RecordingType
, la persona que llama puede hacer clic y ver la documentación (ver Figura 4-1).
Otro mal uso habitual de string
es en los parámetros de las funciones. Supongamos que quieres escribir una función que extraiga todos los valores de un único campo de una matriz. La biblioteca Underscore llama a esto "arrancar":
function
pluck
(
records
,
key
)
{
return
records
.
map
(
r
=>
r
[
key
]);
}
¿Cómo escribirías esto? Aquí tienes un primer intento:
function
pluck
(
records
:any
[],
key
:string
)
:
any
[]
{
return
records
.
map
(
r
=>
r
[
key
]);
}
Este tipo se comprueba pero no es genial. Los tipos any
son problemáticos, sobre todo en el valor de retorno (véase el punto 38). El primer paso para mejorar la firma de tipos es introducir un parámetro de tipo genérico:
function
pluck
<
T
>
(
records
:T
[],
key
:string
)
:
any
[]
{
return
records
.
map
(
r
=>
r
[
key
]);
// ~~~~~~ Element implicitly has an 'any' type
// because type '{}' has no index signature
}
TypeScript se queja ahora de que el tipo string
para key
es demasiado amplio. Y tiene razón al hacerlo: si pasas una matriz de Album
s, entonces sólo hay cuatro valores válidos para key
("artista", "título", "releaseDate" y "recordingType"), frente al vasto conjunto de cadenas. Esto es precisamente lo que es el tipo keyof Album
:
type
K
=
keyof
Album
;
// Type is "artist" | "title" | "releaseDate" | "recordingType"
Así que la solución es sustituir string
por keyof T
:
function
pluck
<
T
>
(
records
:T
[],
key
:keyof
T
)
{
return
records
.
map
(
r
=>
r
[
key
]);
}
Esto pasa el comprobador de tipos. También hemos dejado que TypeScript deduzca el tipo de retorno. ¿Cómo lo hace? Si pasas el ratón por encima de pluck
en tu editor, el tipo inferido es:
function
pluck
<
T
>
(
record
:T
[],
key
:keyof
T
)
:
T
[
keyof
T
][]
T[keyof T]
es el tipo de cualquier valor posible en T
. Si pasas una sola cadena como key
, esto es demasiado amplio. Por ejemplo:
const
releaseDates
=
pluck
(
albums
,
'releaseDate'
);
// Type is (string | Date)[]
El tipo debería ser Date[]
, no (string | Date)[]
. Aunque keyof T
es mucho más estrecho que string
, sigue siendo demasiado amplio. Para estrecharlo aún más, necesitamos introducir un segundo parámetro genérico que sea un subconjunto de keyof T
(probablemente un único valor):
function
pluck
<
T
,
K
extends
keyof
T
>
(
records
:T
[],
key
:K
)
:
T
[
K
][]
{
return
records
.
map
(
r
=>
r
[
key
]);
}
(Para más información sobre extends
en este contexto, véase el Tema 14.)
Ahora la firma de tipo es completamente correcta. Podemos comprobarlo llamando a pluck
de varias formas distintas:
pluck
(
albums
,
'releaseDate'
);
// Type is Date[]
pluck
(
albums
,
'artist'
);
// Type is string[]
pluck
(
albums
,
'recordingType'
);
// Type is RecordingType[]
pluck
(
albums
,
'recordingDate'
);
// ~~~~~~~~~~~~~~~ Argument of type '"recordingDate"' is not
// assignable to parameter of type ...
El servicio lingüístico es capaz incluso de ofrecer autocompletar en las teclas de Album
(como se muestra en la Figura 4-2).
string
tiene algunos de los mismos problemas que any
: cuando se utiliza de forma inadecuada, permite valores no válidos y oculta las relaciones entre tipos. Esto frustra al verificador de tipos y puede ocultar errores reales. La capacidad de TypeScript de definir subconjuntos de string
es una forma poderosa de aportar seguridad de tipos al código JavaScript. Utilizar tipos más precisos detectará errores y mejorará la legibilidad de tu código.
Cosas para recordar
-
Evita el código "stringly typed". Prefiere tipos más apropiados en los que no todas las
string
sean una posibilidad. -
Prefiere una unión de tipos literales de cadena a
string
si eso describe con más precisión el dominio de una variable. Conseguirás una comprobación de tipos más estricta y mejorarás la experiencia de desarrollo. -
Prefiere
keyof T
astring
para los parámetros de función que se espera que sean propiedades de un objeto.
Tema 34: Prefiere los Tipos Incompletos a los Inexactos
En escribiendo declaraciones de tipos encontrarás inevitablemente situaciones en las que puedes modelar el comportamiento de forma más o menos precisa. La precisión en los tipos es generalmente algo bueno porque ayudará a tus usuarios a detectar errores y a aprovechar las herramientas que proporciona TypeScript. Pero ten cuidado al aumentar la precisión de tus declaraciones de tipos: es fácil cometer errores, y los tipos incorrectos pueden ser peor que no tener tipos.
Supongamos que estás escribiendo declaraciones de tipo para GeoJSON, un formato que ya hemos visto en el Tema 31. Una Geometría GeoJSON puede ser de unos cuantos tipos, cada uno de los cuales tiene matrices de coordenadas con formas diferentes:
interface
Point
{
type
:'Point'
;
coordinates
:number
[];
}
interface
LineString
{
type
:'LineString'
;
coordinates
:number
[][];
}
interface
Polygon
{
type
:'Polygon'
;
coordinates
:number
[][][];
}
type
Geometry
=
Point
|
LineString
|
Polygon
;
// Also several others
Esto está bien, pero number[]
para una coordenada es un poco impreciso. En realidad se trata de latitudes y longitudes, por lo que quizá sería mejor un tipo tupla:
type
GeoPosition
=
[
number
,
number
];
interface
Point
{
type
:'Point'
;
coordinates
:GeoPosition
;
}
// Etc.
Publicas tus tipos más precisos al mundo y esperas a que llegue la adulación. Por desgracia, un usuario se queja de que tus nuevos tipos lo han roto todo. Aunque sólo has utilizado la latitud y la longitud, una posición en GeoJSON puede tener un tercer elemento, una elevación y potencialmente más. En un intento de hacer más precisas las declaraciones de tipos, ¡has ido demasiado lejos y has hecho que los tipos sean inexactos! Para seguir utilizando tus declaraciones de tipos, tu usuario tendrá que introducir aserciones de tipos o silenciar por completo el verificador de tipos con as any
.
Como otro ejemplo, considera la posibilidad de intentar escribir declaraciones de tipos para un lenguaje tipo Lisp definido en JSON:
12 "red" ["+", 1, 2] // 3 ["/", 20, 2] // 10 ["case", [">", 20, 10], "red", "blue"] // "red" ["rgb", 255, 0, 127] // "#FF007F"
La biblioteca Mapbox de utiliza un sistema como éste para determinar la apariencia de las características de los mapas en muchos dispositivos. Hay todo un espectro de precisión con el que podrías intentar escribir esto:
-
Permite cualquier cosa.
-
Permite cadenas, números y matrices.
-
Permite cadenas, números y matrices que empiecen por nombres de función conocidos.
-
Asegúrate de que cada función recibe el número correcto de argumentos.
-
Asegúrate de que cada función recibe el tipo correcto de argumentos.
Las dos primeras opciones son sencillas:
type
Expression1
=
any
;
type
Expression2
=
number
|
string
|
any
[];
Más allá de esto, deberías introducir un conjunto de pruebas de expresiones que sean válidas y expresiones que no lo sean. A medida que vayas precisando tus tipos, esto te ayudará a evitar regresiones (véase el punto 52):
const
tests
:Expression2
[]
=
[
10
,
"red"
,
true
,
// ~~~ Type 'true' is not assignable to type 'Expression2'
[
"+"
,
10
,
5
],
[
"case"
,
[
">"
,
20
,
10
],
"red"
,
"blue"
,
"green"
],
// Too many values
[
"**"
,
2
,
31
],
// Should be an error: no "**" function
[
"rgb"
,
255
,
128
,
64
],
[
"rgb"
,
255
,
0
,
127
,
0
]
// Too many values
];
Para pasar al siguiente nivel de precisión, puedes utilizar una unión de tipos literales de cadena como primer elemento de una tupla:
type
FnName
=
'+'
|
'-'
|
'*'
|
'/'
|
'>'
|
'<'
|
'case'
|
'rgb'
;
type
CallExpression
=
[
FnName
,
...
any
[]];
type
Expression3
=
number
|
string
|
CallExpression
;
const
tests
:Expression3
[]
=
[
10
,
"red"
,
true
,
// ~~~ Type 'true' is not assignable to type 'Expression3'
[
"+"
,
10
,
5
],
[
"case"
,
[
">"
,
20
,
10
],
"red"
,
"blue"
,
"green"
],
[
"**"
,
2
,
31
],
// ~~~~~~~~~~~ Type '"**"' is not assignable to type 'FnName'
[
"rgb"
,
255
,
128
,
64
],
[
"rgb"
,
255
,
0
,
127
,
0
]
// Too many values
];
Hay un nuevo error detectado y ninguna regresión. ¡Bastante bien!
¿Qué pasa en si quieres asegurarte de que cada función recibe el número correcto de argumentos? Esto se complica, ya que ahora el tipo debe ser recursivo para llegar a todas las llamadas a funciones. TypeScript lo permite, aunque tenemos que tener cuidado de convencer al verificador de tipos de que nuestra recursión no es infinita. En este caso, eso significa definir CaseCall
(que debe ser una matriz de longitud par) con un interface
en lugar de un type
. Esto es posible, aunque un poco incómodo:
type
Expression4
=
number
|
string
|
CallExpression
;
type
CallExpression
=
MathCall
|
CaseCall
|
RGBCall
;
type
MathCall
=
[
'+'
|
'-'
|
'/'
|
'*'
|
'>'
|
'<'
,
Expression4
,
Expression4
,
];
interface
CaseCall
{
0
:
'case'
;
1
:Expression4
;
2
:Expression4
;
3
:Expression4
;
4?
:Expression4
;
5?
:Expression4
;
// etc.
length
:4
|
6
|
8
|
10
|
12
|
14
|
16
;
// etc.
}
type
RGBCall
=
[
'rgb'
,
Expression4
,
Expression4
,
Expression4
];
const
tests
:Expression4
[]
=
[
10
,
"red"
,
true
,
// ~~~ Type 'true' is not assignable to type 'Expression4'
[
"+"
,
10
,
5
],
[
"case"
,
[
">"
,
20
,
10
],
"red"
,
"blue"
,
"green"
],
// ~~~~~~ ~~~~~~~
// Type '"case"' is not assignable to type '"rgb"'.
// Type 'string' is not assignable to type 'undefined'.
[
"**"
,
2
,
31
],
// ~~~~ Type '"**"' is not assignable to type '"+" | "-" | "/" | ...
[
"rgb"
,
255
,
128
,
64
],
[
"rgb"
,
255
,
0
,
127
,
0
]
// ~ Type 'number' is not assignable to type 'undefined'.
];
Ahora todas las expresiones no válidas producen errores. Y es interesante que puedas expresar algo como "un array de longitud par" utilizando un TypeScript interface
. Pero algunos de estos mensajes de error no son muy buenos, sobre todo el que dice que "case"
no se puede asignar a "rgb"
.
¿Es una mejora respecto a los tipos anteriores, menos precisos? El hecho de que obtengas errores para algunos usos incorrectos es una victoria, pero los mensajes de error confusos harán que sea más difícil trabajar con este tipo. Los servicios lingüísticos forman parte de la experiencia TypeScript tanto como la comprobación de tipos (véase el punto 6), por lo que es una buena idea observar los mensajes de error resultantes de tus declaraciones de tipos y probar el autocompletado en situaciones en las que debería funcionar. Si tus nuevas declaraciones de tipos son más precisas pero rompen el autocompletado, entonces harán que la experiencia de desarrollo de TypeScript sea menos agradable.
La complejidad de esta declaración de tipos también ha aumentado las probabilidades de que se cuele un error. Por ejemplo, Expression4
requiere que todos los operadores matemáticos tomen dos parámetros, pero la especificación de la expresión Mapbox dice que +
y *
pueden tomar más. Además, -
puede tomar un solo parámetro, en cuyo caso niega su entrada. Expression4
indica incorrectamente errores en todos ellos:
const
okExpressions
:Expression4
[]
=
[
[
'-'
,
12
],
// ~~~ Type '"-"' is not assignable to type '"rgb"'.
[
'+'
,
1
,
2
,
3
],
// ~~~ Type '"+"' is not assignable to type '"rgb"'.
[
'*'
,
2
,
3
,
4
],
// ~~~ Type '"*"' is not assignable to type '"rgb"'.
];
Una vez más, al intentar ser más precisos nos hemos pasado y nos hemos vuelto inexactos. Estas imprecisiones pueden corregirse, pero querrás ampliar tu conjunto de pruebas para convencerte de que no se te ha escapado nada más. El código complejo suele requerir más pruebas, y lo mismo ocurre con los tipos.
Al refinar tipos, puede ser útil pensar en la metáfora del "valle misterioso". Refinar tipos muy imprecisos como any
suele ser útil. Pero a medida que tus tipos se hacen más precisos, aumenta la expectativa de que también sean exactos. Empezarás a confiar más en los tipos, por lo que las imprecisiones producirán mayores problemas.
Cosas para recordar
-
Evita el valle misterioso de la seguridad de tipos: los tipos incorrectos suelen ser peores que la ausencia de tipos.
-
Si no puedes modelar un tipo con precisión, ¡no lo modeles con inexactitud! Reconoce las lagunas utilizando
any
ounknown
. -
Presta atención a los mensajes de error y a la función de autocompletar a medida que haces que la tipificación sea cada vez más precisa. No se trata sólo de la corrección: la experiencia del desarrollador también importa.
Tema 35: Generar tipos a partir de API y especificaciones, no de datos
Los otros artículos de en este capítulo han discutido los muchos beneficios de diseñar bien tus tipos y han mostrado lo que puede ir mal si no lo haces. Un tipo bien diseñado hace que TypeScript sea un placer de usar, mientras que uno mal diseñado puede hacerlo miserable. Pero esto ejerce bastante presión sobre el diseño de tipos. ¿No estaría bien que no tuvieras que hacerlo tú mismo?
Es probable que al menos algunos de tus tipos provengan de fuera de tu programa: formatos de archivo, API o especificaciones. En estos casos, puedes evitar escribir tipos generándolos en su lugar. Si lo haces, la clave está en generar tipos a partir de especificaciones, en lugar de a partir de datos de ejemplo. Cuando generas tipos a partir de una especificación, TypeScript te ayudará a asegurarte de que no te has saltado ningún caso. Cuando generas tipos a partir de datos, sólo tienes en cuenta los ejemplos que has visto. Podrías estar pasando por alto casos de perímetro importantes que podrían romper tu programa.
En el Tema 31 escribimos una función para calcular el cuadro delimitador de una Característica GeoJSON de. Esto es lo que parecía:
function
calculateBoundingBox
(
f
:GeoJSONFeature
)
:
BoundingBox
|
null
{
let
box
:BoundingBox
|
null
=
null
;
const
helper
=
(
coords
:any
[])
=>
{
// ...
};
const
{
geometry
}
=
f
;
if
(
geometry
)
{
helper
(
geometry
.
coordinates
);
}
return
box
;
}
El tipo GeoJSONFeature
nunca se definió explícitamente. Podrías escribirlo utilizando algunos de los ejemplos del Tema 31. Pero un enfoque mejor es utilizar la especificación formal GeoJSON.1 Afortunadamente para nosotros, ya existen declaraciones de tipo TypeScript para él en DefinitelyTyped. Puedes añadirlas de la forma habitual:
$ npm install --save-dev @types/geojson + @types/geojson@7946.0.7
Cuando introduces las declaraciones GeoJSON, TypeScript señala inmediatamente un error:
import
{
Feature
}
from
'geojson'
;
function
calculateBoundingBox
(
f
:Feature
)
:
BoundingBox
|
null
{
let
box
:BoundingBox
|
null
=
null
;
const
helper
=
(
coords
:any
[])
=>
{
// ...
};
const
{
geometry
}
=
f
;
if
(
geometry
)
{
helper
(
geometry
.
coordinates
);
// ~~~~~~~~~~~
// Property 'coordinates' does not exist on type 'Geometry'
// Property 'coordinates' does not exist on type
// 'GeometryCollection'
}
return
box
;
}
El problema es que tu código supone que una geometría tendrá una propiedad coordinates
. Esto es cierto para muchas geometrías, incluidos puntos, líneas y polígonos. Pero una geometría GeoJSON también puede ser una GeometryCollection
, una colección heterogénea de otras geometrías. A diferencia de los otros tipos de geometría, no tiene una propiedad coordinates
.
Si llamas a calculateBoundingBox
en una Característica cuya geometría es GeometryCollection
, arrojará un error sobre la imposibilidad de leer la propiedad 0
de undefined
. ¡Esto es un verdadero error! Y lo hemos detectado utilizando definiciones de tipos de una especificación.
Una opción para solucionarlo es desautorizar explícitamente GeometryCollection
s, como se muestra aquí:
const
{
geometry
}
=
f
;
if
(
geometry
)
{
if
(
geometry
.
type
===
'GeometryCollection'
)
{
throw
new
Error
(
'GeometryCollections are not supported.'
);
}
helper
(
geometry
.
coordinates
);
// OK
}
TypeScript es capaz de refinar el tipo de geometry
basándose en la comprobación, por lo que la referencia a geometry.coordinates
está permitida. Aunque sólo sea por eso, el mensaje de error es más claro para el usuario.
Pero ¡la mejor solución es admitir todos los tipos de geometría! Puedes hacerlo sacando otra función auxiliar:
const
geometryHelper
=
(
g
:Geometry
)
=>
{
if
(
geometry
.
type
===
'GeometryCollection'
)
{
geometry
.
geometries
.
forEach
(
geometryHelper
);
}
else
{
helper
(
geometry
.
coordinates
);
// OK
}
}
const
{
geometry
}
=
f
;
if
(
geometry
)
{
geometryHelper
(
geometry
);
}
Si hubieras escrito tú mismo las declaraciones de tipos para GeoJSON, las habrías basado en tu comprensión y experiencia con el formato. Esto podría no haber incluido GeometryCollection
s y te habría llevado a una falsa sensación de seguridad sobre la corrección de tu código. Utilizar tipos basados en una especificación te da la seguridad de que tu código funcionará con todos los valores, no sólo con los que has visto.
Consideraciones similares se aplican a las llamadas a la API: si puedes generar tipos a partir de la especificación de una API, suele ser una buena idea hacerlo. Esto funciona especialmente bien con las API que se tipifican a sí mismas, como GraphQL.
Una API GraphQL viene con un esquema que especifica todas las consultas e interfaces posibles utilizando un sistema de tipos algo similar a TypeScript. Escribes consultas que solicitan campos específicos en estas interfaces. Por ejemplo, para obtener información sobre un repositorio utilizando la API GraphQL de GitHub podrías escribir:
query { repository(owner: "Microsoft", name: "TypeScript") { createdAt description } }
El resultado es:
{
"data"
:
{
"repository"
:
{
"createdAt"
:
"2014-06-17T15:28:39Z"
,
"description"
:
"TypeScript is a superset of JavaScript that compiles to JavaScript."
}
}
}
Lo bueno de este enfoque es que puedes generar tipos TypeScript para tu consulta específica. Al igual que con el ejemplo de GeoJSON, esto ayuda a garantizar que modelas con precisión las relaciones entre tipos y su anulabilidad.
Aquí tienes una consulta para obtener la licencia de código abierto de un repositorio de GitHub:
query getLicense($owner:String!, $name:String!){ repository(owner:$owner, name:$name) { description licenseInfo { spdxId name } } }
$owner
y $name
son variables GraphQL que a su vez están tipadas. La sintaxis de tipos es lo suficientemente similar a la de TypeScript como para que pueda resultar confuso ir de un lado a otro. String
es un tipo GraphQL -sería string
en TypeScript (véase el punto 10). Y mientras que los tipos TypeScript no son anulables, los tipos en GraphQL sí lo son. El !
después del tipo indica que está garantizado que no es nulo.
Hay muchas herramientas para ayudarte a pasar de una consulta GraphQL a tipos TypeScript. Una de ellas es Apollo. Te explicamos cómo utilizarla:
$ apollo client:codegen \ --endpoint https://api.github.com/graphql \ --includes license.graphql \ --target typescript Loading Apollo Project Generating query files with 'typescript' target - wrote 2 files
Necesitas un esquema GraphQL para generar tipos para una consulta. Apollo lo obtiene del punto final api.github.com/graphql
. La salida tiene este aspecto:
export
interface
getLicense_repository_licenseInfo
{
__typename
:
"License"
;
/** Short identifier specified by <https://spdx.org/licenses> */
spdxId
:string
|
null
;
/** The license full name specified by <https://spdx.org/licenses> */
name
:string
;
}
export
interface
getLicense_repository
{
__typename
:
"Repository"
;
/** The description of the repository. */
description
:string
|
null
;
/** The license associated with the repository */
licenseInfo
:getLicense_repository_licenseInfo
|
null
;
}
export
interface
getLicense
{
/** Lookup a given repository by the owner and repository name. */
repository
:getLicense_repository
|
null
;
}
export
interface
getLicenseVariables
{
owner
:string
;
name
:string
;
}
Las partes importantes a tener en cuenta aquí son
-
Se generan interfaces tanto para los parámetros de consulta (
getLicenseVariables
) como para la respuesta (getLicense
). -
La información de anulabilidad se transfiere del esquema a las interfaces de respuesta. Los campos
repository
,description
,licenseInfo
, yspdxId
son anulables, mientras que la licencianame
y las variables de consulta no lo son. -
La documentación se transfiere como JSDoc para que aparezca en tu editor(Tema 48). Estos comentarios proceden del propio esquema GraphQL.
Esta información sobre los tipos ayuda a garantizar que utilizas la API correctamente. Si cambian tus consultas, cambiarán los tipos. Si cambia el esquema, también cambiarán tus tipos. No hay riesgo de que tus tipos y la realidad diverjan, ya que ambos proceden de una única fuente de verdad: el esquema GraphQL.
¿Y si no hay ninguna especificación o esquema oficial disponible? Entonces tendrás que generar tipos a partir de los datos. Herramientas como quicktype
pueden ayudarte con esto. Pero ten en cuenta que tus tipos pueden no coincidir con la realidad: puede haber casos de perímetro que hayas pasado por alto.
Aunque no seas consciente de ello, ya te estás beneficiando de la generación de código. Las declaraciones de tipos de TypeScript para la API DOM del navegador se generan a partir de las interfaces oficiales (ver punto 55). Esto garantiza que modelan correctamente un sistema complicado y ayuda a TypeScript a detectar errores y malentendidos en tu propio código.
Tema 36: Nombra los Tipos Utilizando el Lenguaje de tu Dominio Problemático
Sólo hay dos problemas difíciles en Informática: invalidar la caché y nombrar las cosas.
Phil Karlton
Este libro ha tenido mucho que decir sobre la forma de los tipos y los conjuntos de valores en sus dominios, pero mucho menos sobre cómo nombrar tus tipos. Pero esto también es una parte importante del diseño de tipos. Los nombres de tipos, propiedades y variables bien elegidos pueden aclarar la intención y elevar el nivel de abstracción de tu código y tus tipos. Los tipos mal elegidos pueden oscurecer tu código y conducir a modelos mentales incorrectos.
Supón que estás construyendo una base de datos de animales. Creas una interfaz para representar a uno:
interface
Animal
{
name
:string
;
endangered
:boolean
;
habitat
:string
;
}
const
leopard
:Animal
=
{
name
:
'Snow Leopard'
,
endangered
:false
,
habitat
:
'tundra'
,
};
Aquí hay algunos problemas:
-
name
es un término muy general. ¿Qué tipo de nombre esperas? ¿Un nombre científico? ¿Un nombre común? -
El campo booleano
endangered
también es ambiguo. ¿Qué ocurre si un animal está extinguido? ¿La intención aquí es "en peligro o peor"? ¿O significa literalmente en peligro de extinción? -
El campo
habitat
es muy ambiguo, no sólo por el tipo demasiado ampliostring
(Tema 33), sino también porque no está claro qué se entiende por "hábitat". -
El nombre de la variable es
leopard
, pero el valor de la propiedadname
es "Snow Leopard". ¿Tiene sentido esta distinción?
Aquí tienes una declaración de tipo y valor con menos ambigüedad:
interface
Animal
{
commonName
:string
;
genus
:string
;
species
:string
;
status
:ConservationStatus
;
climates
:KoppenClimate
[];
}
type
ConservationStatus
=
'EX'
|
'EW'
|
'CR'
|
'EN'
|
'VU'
|
'NT'
|
'LC'
;
type
KoppenClimate
=
|
'Af'
|
'Am'
|
'As'
|
'Aw'
|
'BSh'
|
'BSk'
|
'BWh'
|
'BWk'
|
'Cfa'
|
'Cfb'
|
'Cfc'
|
'Csa'
|
'Csb'
|
'Csc'
|
'Cwa'
|
'Cwb'
|
'Cwc'
|
'Dfa'
|
'Dfb'
|
'Dfc'
|
'Dfd'
|
'Dsa'
|
'Dsb'
|
'Dsc'
|
'Dwa'
|
'Dwb'
|
'Dwc'
|
'Dwd'
|
'EF'
|
'ET'
;
const
snowLeopard
:Animal
=
{
commonName
:
'Snow Leopard'
,
genus
:
'Panthera'
,
species
:
'Uncia'
,
status
:
'VU'
,
// vulnerable
climates
:
[
'ET'
,
'EF'
,
'Dfd'
],
// alpine or subalpine
};
Esto supone una serie de mejoras:
-
name
ha sido sustituido por términos más específicos:commonName
,genus
, yspecies
. -
endangered
se ha convertido enstatus
, un tipo deConservationStatus
que utiliza un sistema de clasificación estándar de la UICN. -
habitat
se ha convertido enclimates
y utiliza otra taxonomía estándar, la clasificación climática de Köppen.
Si necesitaras más información sobre los campos de la primera versión de este tipo, tendrías que ir a buscar a la persona que los escribió y preguntarle. Lo más probable es que haya dejado la empresa o no se acuerde. Peor aún, podrías consultar git blame
para averiguar quién escribió estos pésimos tipos, ¡y descubrir que fuiste tú!
La situación ha mejorado mucho con la segunda versión. Si quieres saber más sobre el sistema de clasificación climática de Köppen o averiguar cuál es el significado exacto de un estado de conservación, existen innumerables recursos en Internet que te ayudarán.
Cada dominio tiene un vocabulario especializado para describir su tema. En lugar de inventar tus propios términos, intenta reutilizar términos del dominio de tu problema. A menudo, estos vocabularios se han perfeccionado a lo largo de años, décadas o siglos, y son bien comprendidos por la gente del sector. Utilizar estos términos te ayudará a comunicarte con los usuarios y aumentará la claridad de tus tipos.
Ten cuidado de utilizar el vocabulario del dominio con precisión: cooptar el lenguaje de un dominio para que signifique algo diferente es aún más confuso que inventar el tuyo propio.
Aquí tienes otras reglas que debes tener en cuenta cuando nombres tipos, propiedades yvariables:
-
Haz que las distinciones tengan sentido. Al escribir y hablar puede resultar tedioso utilizar la misma palabra una y otra vez. Introducimos sinónimos para romper la monotonía. Esto hace que la prosa sea más agradable de leer, pero tiene el efecto contrario en el código. Si utilizas dos términos diferentes, asegúrate de que haces una distinción significativa. Si no es así, debes utilizar el mismo término.
-
Evita nombres vagos y sin sentido como "datos", "información", "cosa", "elemento", "objeto" o la siempre popular "entidad". Si Entidad tiene un significado específico en tu dominio, bien. Pero si lo utilizas porque no se te ocurre un nombre más significativo, al final tendrás problemas.
-
Nombra las cosas por lo que son, no por lo que contienen o por cómo se calculan.
Directory
es más significativo queINodeList
. Te permite pensar en un directorio como un concepto, en lugar de en términos de su implementación. Unos buenos nombres pueden aumentar tu nivel de abstracción y disminuir el riesgo decolisiones involuntarias.
Tema 37: Considerar las "Marcas" para la Tipificación Nominal
En el punto 4 se habló de tipificación estructural ("pato") y de cómo a veces puede conducir a resultados sorprendentes:
interface
Vector2D
{
x
:number
;
y
:number
;
}
function
calculateNorm
(
p
:Vector2D
)
{
return
Math
.
sqrt
(
p
.
x
*
p
.
x
+
p
.
y
*
p
.
y
);
}
calculateNorm
({
x
:3
,
y
:4
});
// OK, result is 5
const
vec3D
=
{
x
:3
,
y
:4
,
z
:1
};
calculateNorm
(
vec3D
);
// OK! result is also 5
¿Y si quisieras que calculateNorm
rechazara los vectores 3D? Esto va en contra del modelo de tipado estructural de TypeScript, pero sin duda es más correcto desde el punto de vista matemático.
Una forma de conseguirlo es con la tipificación nominal. Con la tipificación nominal, un valor es un Vector2D
porque tú dices que lo es, no porque tenga la forma correcta. Para aproximarte a esto en TypeScript, puedes introducir una "marca" (piensa en vacas, no en Coca-Cola):
interface
Vector2D
{
_brand
:
'2d'
;
x
:number
;
y
:number
;
}
function
vec2D
(
x
:number
,
y
:number
)
:
Vector2D
{
return
{
x
,
y
,
_brand
:
'2d'
};
}
function
calculateNorm
(
p
:Vector2D
)
{
return
Math
.
sqrt
(
p
.
x
*
p
.
x
+
p
.
y
*
p
.
y
);
// Same as before
}
calculateNorm
(
vec2D
(
3
,
4
));
// OK, returns 5
const
vec3D
=
{
x
:3
,
y
:4
,
z
:1
};
calculateNorm
(
vec3D
);
// ~~~~~ Property '_brand' is missing in type...
La marca garantiza que el vector procede del lugar correcto. Por supuesto, nada te impide añadir _brand: '2d'
al valor vec3D
. Pero esto es pasar de lo accidental a lo malicioso. Este tipo de marca suele bastar para detectar usos inadvertidos de funciones.
Curiosamente, puedes obtener muchas de las mismas ventajas que las marcas explícitas operando sólo en el sistema de tipos. Esto elimina la sobrecarga en tiempo de ejecución y también te permite marcar tipos incorporados como string
o number
, a los que no puedes añadir propiedades adicionales.
Por ejemplo, ¿qué ocurre si tienes una función que opera en el sistema de archivos y requiere una ruta absoluta (en lugar de relativa)? Esto es fácil de comprobar en tiempo de ejecución (¿la ruta empieza por "/"?), pero no tanto en el sistema de tipos.
He aquí un enfoque con marcas:
type
AbsolutePath
=
string
&
{
_brand
:
'abs'
};
function
listAbsolutePath
(
path
:AbsolutePath
)
{
// ...
}
function
isAbsolutePath
(
path
:string
)
:
path
is
AbsolutePath
{
return
path
.
startsWith
(
'/'
);
}
No puedes construir un objeto que sea un string
y tenga una propiedad _brand
. Esto es puramente un juego con el sistema de tipos.
Si tienes una ruta string
que puede ser absoluta o relativa, puedes comprobarla utilizando la guarda de tipo, que refinará su tipo:
function
f
(
path
:string
)
{
if
(
isAbsolutePath
(
path
))
{
listAbsolutePath
(
path
);
}
listAbsolutePath
(
path
);
// ~~~~ Argument of type 'string' is not assignable
// to parameter of type 'AbsolutePath'
}
Este tipo de enfoque podría ser útil para documentar qué funciones esperan rutas absolutas o relativas y qué tipo de ruta contiene cada variable. Sin embargo, no es una garantía férrea: path as AbsolutePath
tendrá éxito para cualquier string
. Pero si evitas este tipo de afirmaciones, la única forma de obtener un AbsolutePath
es que te lo den o comprobarlo, que es exactamente lo que quieres.
Este enfoque puede utilizarse para modelar muchas propiedades que no pueden expresarse dentro del sistema de tipos. Por ejemplo, utilizar la búsqueda binaria para encontrar un elemento en una lista:
function
binarySearch
<
T
>
(
xs
:T
[],
x
:T
)
:
boolean
{
let
low
=
0
,
high
=
xs
.
length
-
1
;
while
(
high
>=
low
)
{
const
mid
=
low
+
Math
.
floor
((
high
-
low
)
/
2
);
const
v
=
xs
[
mid
];
if
(
v
===
x
)
return
true
;
[
low
,
high
]
=
x
>
v
?
[
mid
+
1
,
high
]
:
[
low
,
mid
-
1
];
}
return
false
;
}
Esto funciona si la lista está ordenada, pero dará falsos negativos si no lo está. No puedes representar una lista ordenada en el sistema de tipos de TypeScript. Pero puedes crear una marca:
type
SortedList
<
T
>
=
T
[]
&
{
_brand
:
'sorted'
};
function
isSorted
<
T
>
(
xs
:T
[])
:
xs
is
SortedList
<
T
>
{
for
(
let
i
=
1
;
i
<
xs
.
length
;
i
++
)
{
if
(
xs
[
i
]
<
xs
[
i
-
1
])
{
return
false
;
}
}
return
true
;
}
function
binarySearch
<
T
>
(
xs
:SortedList
<
T
>
,
x
:T
)
:
boolean
{
// ...
}
Para llamar a esta versión de binarySearch
, necesitas que te den un SortedList
(es decir, tener una prueba de que la lista está ordenada) o demostrar tú mismo que está ordenada utilizando isSorted
. La exploración lineal no es muy buena, ¡pero al menos estarás a salvo!
Ésta es una perspectiva útil sobre el verificador de tipos en general. Para llamar a un método de un objeto, por ejemplo, necesitas que te den un objeto nonull
o demostrar tú mismo que es nonull
con una condicional.
También puedes marcar number
tipos-por ejemplo, para unir unidades:
type
Meters
=
number
&
{
_brand
:
'meters'
};
type
Seconds
=
number
&
{
_brand
:
'seconds'
};
const
meters
=
(
m
:number
)
=>
m
as
Meters
;
const
seconds
=
(
s
:number
)
=>
s
as
Seconds
;
const
oneKm
=
meters
(
1000
);
// Type is Meters
const
oneMin
=
seconds
(
60
);
// Type is Seconds
Esto puede resultar incómodo en la práctica, ya que las operaciones aritméticas hacen que los números olviden sus marcas:
const
tenKm
=
oneKm
*
10
;
// Type is number
const
v
=
oneKm
/
oneMin
;
// Type is number
Sin embargo, si tu código implica muchos números con unidades mixtas, éste puede seguir siendo un enfoque atractivo para documentar los tipos esperados de parámetros numéricos.
Cosas para recordar
-
TypeScript utiliza tipado estructural ("pato"), que a veces puede dar lugar a resultados sorprendentes. Si necesitas tipado nominal, considera la posibilidad de adjuntar "marcas" a tus valores para distinguirlos.
-
En algunos casos puedes adjuntar marcas enteramente en el sistema de tipos, en lugar de en tiempo de ejecución. Puedes utilizar esta técnica para modelar propiedades fuera del sistema de tipos de TypeScript.
1 GeoJSON también se conoce como RFC 7946. La especificación, muy legible, está en http://geojson.org.
Get TypeScript eficaz now with the O’Reilly learning platform.
O’Reilly members experience books, live events, courses curated by job role, and more from O’Reilly and nearly 200 top publishers.