TypeScript: estrechamiento del tipo de retorno basado en un argumento de opción booleano
Estoy tratando de crear un tipo para mi función de modo que el tipo de retorno cambie según un valor que se le pase. He considerado un tipo de retorno en Typescript dependiendo del parámetro y seguir eso (casi) al pie de la letra parece funcionar bastante bien. Sin embargo, en mi proyecto, debo usar funciones flecha. Además, me gustaría evitar sobrecargas si es posible. Considera esta firma e implementación:
type CreateDocumentListOptions = {
withVersions?: boolean;
};
type CreateDocumentList = (
options?: CreateDocumentListOptions
) => CreateDocumentListOptions["withVersions"] extends true
? DocumentWithVersions[]
: Document[];
const createDocumentList: CreateDocumentList = ({
withVersions = false
} = {}) =>
times(
withVersions ? createDocumentWithVersions : createDocument,
chance.natural({ min: 1, max: 10 })
);
cuando llamo a la función así:
const documentList: DocumentWithVersions[] = createDocumentList({
withVersions: true
});
Recibo un desajuste de tipos ya que TS piensa que createDocumentList devuelve un Document[] y no el deseado DocumentWithVersions[]. Tenía la impresión de que usar el operador ternario sería suficiente información para que TS entendiera que el tipo de retorno cambiaría según su condición.
Para el ejemplo editable completo:
Respuesta #1
Si estás renunciando a las sobrecargas, pero aún quieres un tipo de retorno que cambie dependiendo del parámetro, entonces necesitarás usar una función genérica que devuelva un tipo condicional. Por ejemplo:
type CreateDocumentList = <B extends boolean = false>(
options?: { withVersions?: B }
) => B extends true
? DocumentWithVersions[]
: Document[];
Aquí el parámetro de tipo genérico B está restringido a boolean y por defecto es false. Luego, dependiendo de si B es true o false, la salida es ya sea DocumentWithVersions[] o Document[].
Una advertencia con las funciones genéricas que devuelven tipos condicionales es que, dentro de la implementación de la función, el compilador no puede verificar que lo que estás haciendo sea seguro. Donde B es un genérico no especificado, el compilador aplaza la evaluación de B extends true ? ..., lo que significa que no puede ver cuándo un valor particular coincide con el tipo. Este punto problemático es el tema del problema abierto microsoft/TypeScript#33912.
const createDocumentListOops: CreateDocumentList = (({
withVersions = false
} = {}) =>
times(
withVersions ? createDocumentWithVersions : createDocument,
chance.natural({ min: 1, max: 10 })
)); // error!
// Type 'Document[]' is not assignable to type
// 'B extends true ? Required<Document>[] : Document[]'
Hasta que y a menos que se haga algo para abordar esto, significa que necesitarás usar afirmaciones de tipo o algo similar para evitar errores del compilador en la implementación:
const createDocumentList = (({
withVersions = false
} = {}) =>
times(
withVersions ? createDocumentWithVersions : createDocument,
chance.natural({ min: 1, max: 10 })
)) as CreateDocumentList; // okay
Ahora podemos probar que las llamadas a createDocumentList() se comportan como deseas:
const documentList = createDocumentList({ withVersions: true });
// const documentWithVersionsList: Required<Document>[]
const documentList1 = createDocumentList();
// const documentList1: Document[]
const documentList2 = createDocumentList({ withVersions: false });
// const documentList2: Document[]
const documentList3 = createDocumentList({ withVersions: Math.random() < 0.5 });
// const documentList3: Document[] | Required<Document>[]
Se ve bien. Nota que documentList3 es una unión de tipos porque la propiedad withVersions del input es boolean, que es la unión true | false, y el tipo de retorno de createDocumentList() es distributivo sobre uniones. Esto puede o no ser el comportamiento que deseas, pero me parece razonable.
Enlace de Playground al código