typescript - ¿es posible acotar un genérico con un typeguard?
Estoy intentando acotar un genérico, donde la autocompletación detecta que está alcanzando un typeguard, y por lo tanto no volverá a llegar a ese mismo bloque de código. Creo que la razón es que no estoy restringiendo el tipo al Genérico, pero no estoy seguro de cómo hacerlo, ¿es siquiera posible? Parece que debería ser posible, pero no estoy seguro. Cualquier ayuda sería apreciada.
// Setup
export type Feature<Geometry> = {
type: 'Feature',
geometry: Geometry
}
type Geometry = Point | Curve
interface Base {
type: string
}
interface Point extends Base{
type: 'Point'
}
interface Curve extends Base {
type: 'Curve'
}
// Typeguard
function isGeometry<G extends Geometry, U extends G['type']>(geometry: G, disciminator: U): geometry is Extract<G, {type: U}>{
return geometry.type === disciminator
}
function isFeature<G extends Geometry, U extends G['type']>(feature: Feature<G>, disciminator: U): feature is Feature<Extract<G, {type: U}>> {
return feature.geometry.type === disciminator
}
function whatGeometry(feature: Feature<Point | Curve>) {
if(isGeometry(feature.geometry, 'Curve')){
return feature.geometry;
// ^?
}
if(isGeometry(feature.geometry, 'Point')){
return feature.geometry;
// ^?
} // Autocompletes, and knows that we can't have anything else for a geometry,
return;
}
function whatFeature(feature: Feature<Point | Curve>) {
if(isFeature(feature, 'Curve')){
return feature.geometry;
// ^?
}
if(isFeature(feature, 'Point')) {
return feature;
// ^?
} // Assumes we can have another Feature<Point> even though the upper typeguard should have caught it
return;
}
Patio de recreo
Respuesta #1
El problema principal que estás enfrentando es que las funciones de guardia de tipo definidas por el usuario, como isFeature, solo te permiten especificar exactamente lo que debería ocurrir si devuelven true, que es restringir el tipo del argumento relevante al tipo protegido. Así que para una función cuyo tipo de retorno es arg is Type, sabemos que para un resultado true, arg se restringirá a algo como typeof arg & Type, o Extract<typeof arg, Type> (usando el tipo de utilidad Extract), dependiendo del tipo que tenga arg. En términos generales, si typeof arg es un tipo unión, entonces obtienes un comportamiento similar a Extract; de lo contrario, obtienes un comportamiento similar a la intersección. (Estoy diciendo "similar" muchas veces aquí porque el comportamiento real es un poco más sutil y no quiero desviarme demasiado.)
Pero cuando la función devuelve false, el compilador tiene que intentar determinar qué hacer por sí mismo. No existen funciones de guardia de tipos "unilaterales" o "de grano fino" como se solicita en microsoft/TypeScript#15048 que te permitan decir arg is TrueType else FalseType o algo por el estilo. En este momento, lo que sucede es que, si arg tiene un tipo unión, entonces obtienes algo como Exclude<typeof arg, Type>, lo que en realidad filtra arg. Pero si arg no tiene un tipo unión, entonces simplemente obtienes typeof arg. TypeScript carece de tipos negados como not Type, por lo que no existe un tipo equivalente a typeof arg & not Type similar a typeof arg & Type al que se pueda restringir en el caso false. (Hubo una implementación de tipos negados en microsoft/TypeScript#29317, pero nunca se adoptó.) Por lo tanto, lo mejor que el compilador puede hacer en general es dejar arg tal cual.
Eso conduce directamente al comportamiento que estás viendo con isFeature. El tipo de entrada Feature<Point | Curve> no es un tipo unión, así que cuando isFeature devuelve falso, no hay mucho que el compilador pueda hacer con el tipo de la entrada.
Si quieres poder enfocarte en la rama false, deberías considerar hacer que el tipo de entrada sea una unión como Feature<Point> | Feature<Curve>. Es más complicado hacer esto, ya que tu isFeature() actual se negaría a aceptar una entrada de ese tipo. Pero podrías refactorizar para permitir entradas de unión, posiblemente así:
function isFeature<T extends Geometry['type'], U extends T>(
feature: Feature<Geometry & { type: T }>, disciminator: U
): feature is Feature<Geometry & { type: U }> {
return feature.geometry.type === disciminator
}
function whatFeature(feature: Feature<Point> | Feature<Curve>) {
if (isFeature(feature, 'Curve')) {
return feature.geometry;
// ^?(parameter) feature: Feature<Curve>
}
feature;
// ^?(parameter) feature: Feature<Point>
if (isFeature(feature, 'Point')) {
return feature;
// ^?(parameter) feature: Feature<Point>
}
return;
}
Entonces feature es Feature<Point> | Feature<Curve>, lo que hace posible que un resultado de guardia de tipo negativo lo reduzca. Y isFeature() es genérico en el tipo T, lo que permite entradas de unión. Cuando devuelve true, el compilador reduce la entrada solo a aquellos miembros de la unión cuyo tipo de geometría es U, y por lo tanto un resultado false reduce la entrada al complemento de esos miembros.
Eso te da el comportamiento que deseas, donde feature se reduce a Feature<Point> después de una comprobación negativa de isFeature(feature, 'Curve'), como se desea.
Enlace de Playground al código
