Me cuesta entender la verificación de tipos en el contexto del valor de retorno de una función.
Enlace del patio de recreo
Veamos algo que sabemos que es válido:
interface Foo {
bar: string
}
const foo: Foo = { bar: 'bar' }
// Good!
Luego experimente con las excepciones esperadas:
// Missing property = Bad
const nobar: Foo = { }
// Property 'bar' is missing in type '{}' but required in type 'Foo'.(2741)
// Extra property = Bad
const foobar: Foo = { bar: 'bar', extra: 1 }
// Type '{ bar: string; extra: number; }' is not assignable to type 'Foo'.
// Object literal may only specify known properties, and 'extra' does not exist in type 'Foo'.(2322)
Y luego introduce Foo como el tipo de retorno de una función:
// Perfection!
const fooFunc: () => Foo = () => ({ bar: 'bar' })
// Missing property = Bad
const nobarFunc: () => Foo = () => ({ })
// Property 'bar' is missing in type '{}' but required in type 'Foo'.(2741)
// Extra property = Bad
const foobarFunc: () => Foo = () => ({ bar: 'bar', extra: 1 })
// Sadly, not caught by typescript
En mi caso de uso, la función es una devolución de llamada y quiero que el tipo de devolución se valide en la función (que el valor de devolución sea compatible con Foo).
Si agrego un tipo de retorno explícito a la definición de la función de flecha, obtengo la validación esperada (marcadores para mostrar lo que he agregado):
// Extra property = Bad
const foofooFunc: () => Foo = ()/*>>*/: Foo/*<<*/ => ({ bar: 'bar', extra: 1 })
// Type '{ bar: string; extra: number; }' is not assignable to type 'Foo'.
// Object literal may only specify known properties, and 'extra' does not exist in type 'Foo'.(2322)
Originalmente me encontré con esto en el contexto de una función genérica que recibe una devolución de llamada que debe escribirse en el valor de retorno.
function process<T>(callback: () => T): T {
return callback()
}
// Missing property, but generic not specified (so inferred from callback return)
process(() => ({ }))
// Extra property, but generic not specified (so inferred from callback return)
process(() => ({ bar: 'bar', extra: 1 }))
// Missing property, but generic specified, return type validation applied
process<Foo>(() => ({ }))
// Property 'bar' is missing in type '{}' but required in type 'Foo'.(2741)
// Extra property, generic specified, but no error
process<Foo>(() => ({ bar: 'bar', extra: 1 }))
// Sadly, not caught by typescript
// Extra property, generic specified, callback explicitly defines return type, error raised
process<Foo>((): Foo => ({ bar: 'bar', extra: 1 }))
// Type '{ bar: string; extra: number; }' is not assignable to type 'Foo'.
// Object literal may only specify known properties, and 'extra' does not exist in type 'Foo'.(2322)
Parece que en el caso de un tipo de valor devuelto, el tipo es aceptable si tiene al menos la misma forma, a diferencia de exactamente la misma forma en el caso de la asignación directa.
Aprecio que una interfaz sea esencialmente un subconjunto de un tipo más amplio, pero parece haber una diferencia en cómo se aplica esto (a veces su estricta igualdad, otras veces una reducción).
También entiendo que este es probablemente el comportamiento previsto, pero estoy un poco sorprendido de que el tipo especificado en el genérico no se aplique a la devolución de llamada, es como si el tipo fuera forzado porque en su lugar tiene "al menos la misma forma". de igual Eso podría deberse a que hay casos genuinamente válidos en los que se reduce el tipo de retorno.
Obviamente, mi aplicación es más compleja que esto, y aunque escribir la devolución de llamada funciona, es fácil de olvidar y luego existe el riesgo de que no se aplique ninguna verificación de tipo (tanto el tipo genérico como la devolución de llamada deben especificarse). Además, la verbosidad adicional lo convierte en una ensalada de palabras y ofusca el resto de la intención.
No es posible especificar una extends
en el genérico como se pretende que sea, bueno, ¡genérico! es decir, no hay ningún tipo basado más allá de object
. Además, la devolución de llamada tiene parámetros de entrada, pero estos también son genéricos, sin un tipo base significativo.
Hay una pregunta relacionada pero algo mal nombrada ( ¿Son posibles las funciones fuertemente tipadas como parámetros en TypeScript? ) Que solo se trata de cómo escribir una función aparte de function
.
Solución del problema
Creo que, como ya ha mencionado, ese es el comportamiento esperado. Probablemente desee poder asignar métodos que no coincidan del todo pero que se superpongan como su devolución de llamada, como por ejemplo:
function withCallbackA(cb: () => {key: string, key2: string}): unknown {
// do sth.
}
function withCallbackB(cb: () => {key: string}): unknown {
// do sth.
}
function callback(): {key: string, key2: string} {
// do sth.
}
withCallbackA(callback)
withCallbackB(callback)
Si no desea ese comportamiento, podría usar algo similar a
function withCallbackC(cb: () => {key1: string} & Record<string|number,never>): unknown {
// do sth.
}
withCallbackC(callback) // will give something along the lines of "Argument of type '() => { key1: string; key2: string; }' is not assignable to parameter of type '() => {key1: string} & Record<string | number, never>'."
y escriba explícitamente otras claves como nunca o inexistentes.
En el caso del ejemplo anterior, podría usar un tipo como {key1: string, [key in string| number]: never}
en su lugar, pero como eso no funcionaría con su interfaz, recomendaría atenerse al... & Record<string|number, never>
No hay comentarios:
Publicar un comentario