Búsqueda de sitios web

Restringir los valores posibles de una entrada a un tipo derivado basado en la entrada de un tipo anterior (TypeScript)


En la programación funcional con tipos, a menudo puedes restringir los tipos en tu aplicación a los valores proporcionados de entrada.

Sin embargo, estoy perdido en cómo hacer esto con TypeScript con los posibles valores de las claves para un Map.

Considera lo siguiente:

const match = <T, V, R>(p : (x : V) => T, ... c : Array< [T, (x : V) => R] >) : (x : V) => R => {
  const cases = new Map(c);
  return (x : any) => cases.get(p(x))(x);
}

Un formato más legible para los humanos:

const match = <Row, Value, ReturnType>(p : (x : Value) => Row, ...c : Array< [Row, (x : Value) => ReturnType] >) : (x : Value) => ReturnType => {
  const cases = new Map(c);
  return x => cases.get(p(x))(x);
}

Sin embargo, hay un problema con esta notación ya que en realidad no está haciendo lo que se pretende hacer:

match(x => Math.random() > 0.5 ? "foo" : "bar", ["baz", x => x])

Esto debería, en un sistema de Tipado suficientemente avanzado, poder considerarse un error; sin embargo, en TypeScript no está detectando que los valores "foo" o "bar" no son ninguno de los valores esperados y en realidad harían que el código lanzara un error.

Un ejemplo más detallado:

const validDouble = match( x => x * 2,
[4, x => "four"],
[6, x => "six"]
)

validDouble(4) // TS ERROR: 8 is not a valid value for Type ROW (4 | 6)
validDouble(2) // "four", no TS Error

Sé que no puedo imponer que el parámetro rest sea un valor obligatorio de arreglos (sin embargo, podría envolverlo en un arreglo obligatorio y requerir caracteres extra del desarrollador), pero tengo curiosidad si hay una manera de hacer cumplir que los valores en la fila coincidan con los valores de salida en la función predicado.

EDITAR: Sí, sé que esto lanza un error con una entrada que no coincide. Esa lógica es FÁCIL de insertar con condicionales o bloques try-catch y convertir esto en un matchMaybe, sin embargo, este ejemplo fue simplificado porque eso no tiene impacto ni implicación en el sistema de tipos disponible en TypeScript

Respuesta #1

Ok, aquí está la función arreglada (puede necesitar algo de limpieza y probablemente mejoras).

const match = <
Row extends string | number,
Value,
ReturnType,
V extends (x: Value) => ReturnType,
C extends [...[Row, V][]],
>
(
predicate: (x: Value) => Row,
...c: C & [...[Row, V][]]
): (x: Value) => ReturnType => {

const cases = new Map<Row, V>(c);

return x =>
// put a runtime check below instead of asserting 
    // non-undefined value with `!` 
    cases.get(predicate(x))!(x)
}

match(x => 'baz', ["baz", (x: any) => x]) // ok
match(x => 'qwe', ["baz", (x: any) => x]) // err
match(x => 'qwe', ["baz", (x: any) => x], ["qwe", (x: any) => x]) //ok

Parque infantil

El primer problema fue que el parámetro rest debería ser una tupla. El segundo problema fue colocar los parámetros de tipo correctamente para que todo se infiera correctamente.

P.D. Los tipos complejos en TypeScript son difíciles. Siento que hay falta de documentación sobre los parámetros de tipo, por lo tanto siempre me siento inseguro sobre ellos (creo que otros también lo hacen).