¿Cómo anotar una función con múltiples firmas de llamada posibles en Flow?


En JavaScript, es común tener una función que puede ser llamada de más de una manera – por ejemplo, con un puñado de argumentos posicionales o un solo objeto de opciones o alguna combinación de los dos.

He estado tratando de averiguar cómo anotar esto.

Una forma que intenté fue anotar args de descanso como una unión de varias tuplas posibles:

type Arguments =
  | [string]
  | [number]
  | [string, number]
;

const foo = (...args: Arguments) => {
  let name: string;
  let age: number;

  // unpack args...
  if (args.length > 1) {
    name = args[0];
    age = args[1];
  } else if (typeof args[0] === 'string') {
    name = args[0];
    age = 0;
  } else {
    name = 'someone';
    age = args[1];
  }

  console.log(`${name} is ${age}`);
};

// any of these call signatures should be OK:
foo('fred');
foo('fred', 30);
foo(30);

El fragmento de código anterior es artificial; probablemente podría usar (...args: Array<string | number>) en este ejemplo, pero para firmas (por ejemplo, involucrando un objeto de opciones escrito que puede estar solo o con args anteriores) sería útil ser capaz de definir un conjunto preciso y finito de posibles firmas de llamada.

Pero lo anterior no escribe-check. Puedes ver un montón de errores confusos en tryflow .

También intenté escribir la función en sí como una unión de defs de función completa separada, pero eso tampoco funcionó :

type FooFunction =
  | (string) => void
  | (number) => void
  | (string, number) => void
;

const foo: FooFunction = (...args) => {
  let name: string;
  let age: number;

  // unpack args...
  if (args.length > 1) {
    name = args[0];
    age = args[1];
  } else if (typeof args[0] === 'string') {
    name = args[0];
    age = 0;
  } else {
    name = 'someone';
    age = args[1];
  }

  console.log(`${name} is ${age}`);
};

// any of these call signatures should be OK:
foo('fred');
foo('fred', 30);
foo(30);

¿ Cómo debo abordar las funciones de anotaciones de tipo con ¿múltiples firmas de llamada posibles? (O las firmas múltiples se consideran un anti-patrón en el Flujo, y simplemente no debería hacerlo en absoluto, en cuyo caso, ¿cuál es el enfoque recomendado para interactuar con bibliotecas de terceros que lo hacen?)

Author: callum, 2017-04-28

3 answers

Los errores que está viendo son una combinación de un error en su código y un error en Flow.

Error en el código

Comencemos por corregir su error. En la tercera sentencia else, se asigna el valor incorrecto a

  } else {
    name = 'someone';
    age = args[1]; // <-- Should be index 0
  }

Cambiar el acceso al array para que sea el índice correcto elimina dos errores. Creo que ambos podemos estar de acuerdo en que esto es exactamente para lo que está Flow, encontrar errores en su código.

Tipo de estrechamiento

Para llegar a la causa raíz del problema, podemos ser más explícito en el área donde están los errores para que podamos ver más fácilmente cuál es el problema:

if (args.length > 1) {
  const args_tuple: [string, number] = args;
  name = args_tuple[0];
  age = args_tuple[1];
} else if (typeof args[0] === 'string') {

Esto es efectivamente lo mismo que antes, pero porque estamos muy claros sobre lo que args[0] y args[1] deberían ser en este punto. Esto nos deja con un solo error.

Error en el flujo

El error restante es un error en Flow: https://github.com/facebook/flow/issues/3564

Error: el tipo de tupla no interactúa con las aserciones de longitud (.longitud >= 2 y [] | [número] / [número, número] tipo)

Cómo escribir funciones sobrecargadas

Flow no es muy bueno para tratar con variádicas con diferentes tipos, como en este caso. Variadics son más para cosas como function sum(...args: Array<number>) donde todos los tipos son iguales y no hay arity máximo.

En cambio, deberías ser más explícito con tus argumentos, así:

const foo = (name: string | number, age?: number) => {
  let real_name: string = 'someone';
  let real_age: number = 0;

  // unpack args...
  if (typeof name === 'number') {
    real_age = name;
  } else {
    real_name = name;
    real_age = age || 0;
  }

  console.log(`${real_name} is ${real_age}`);
};

// any of these call signatures should be OK:
foo('fred');
foo('fred', 30);
foo(30);

Esto no causa errores y creo que es más fácil de leer para los desarrolladores, demasiado.

Una mejor manera

En otra respuesta, Pavlo proporcionó otra solución que me gusta más que la mía.

type Foo =
  & ((string | number) => void)
  & ((string, number) => void)

const foo: Foo = (name, age) => {...};

Resuelve los mismos problemas de una manera mucho más limpia, lo que le permite mucha más flexibilidad. Al crear una intersección de múltiples tipos de funciones, describe cada forma diferente de llamar a su función, permitiendo que Flow pruebe cada una en función de cómo se llama a la función.

 8
Author: EugeneZ,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2017-11-06 17:25:46

Puede definir múltiples firmas de función uniéndolas con &:

type Foo =
  & ((string | number) => void)
  & ((string, number) => void)

Inténtalo.

 5
Author: Pavlo,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2017-08-23 15:31:01

De los tres entrenamientos posibles que dio, he descubierto cómo hacer que funcione utilizando un solo objeto de opciones, sin embargo, debido a que requiere al menos un objeto para ser establecido, es necesario definir cada posibilidad.

Así:

type Arguments = 
    {|
        +name?: string,
        +age?: number
    |} |
    {|
        +name: string,
        +age?: number
    |} |
    {|
        +name?: string,
        +age: number
    |};

const foo = (args: Arguments) => {
  let name: string = args.name ? args.name : 'someone';
  let age: number = typeof args.age === 'number' && !isNaN(args.age) ? args.age : 0;
  console.log(`${name} is ${age}`);
}

// any of these call signatures are OK:
foo({ name: 'fred' });
foo({ name: 'fred', age: 30 });
foo({ age: 30 });

// fails
foo({});
 1
Author: Wilco Bakker,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2017-05-03 06:50:01