typescript - ¿Cuál es el propósito de inferir un tipo en un tipo condicional solo para devolver el tipo inferido?
He estado mirando esta publicación así como esta monstruosa respuesta a otra publicación, lo que ha despertado mi curiosidad por entender los tipos condicionales, los tipos condicionales distributivos y la palabra clave infer.
He revisado las siguientes publicaciones relacionadas con la palabra clave infer:
- ¿Por qué se necesita la palabra clave infer en Typescript?
- TypeScript: ¿Cuál es el contexto de P en `T extends infer P`? (no está claro de dónde viene `P`)
- Tratando de entender los límites de 'T extends infer U'
En este punto, mi comprensión es que:
- Los tipos condicionales se utilizan cuando desea cambiar de manera flexible el tipo de retorno esperado dado algún criterio. Por ejemplo, si una variable extiende
number, entonces devuelve1; de lo contrario, devuelve'string'. inferse utiliza para decirle al compilador "haz tu mejor conjetura sobre cuál debería ser el tipo de esta variable"
¿Por qué y cuándo es útil (o necesario) escribir algo como lo siguiente?:
type GenericType<T> = T extends infer R ? R : never;
Estoy suponiendo que es útil/necesario (y no solo se usa con fines ilustrativos) basándome en los 3 mensajes anteriores que he enlazado.
Puedo entender que esto esencialmente le dice al compilador "Confirma que el tipo genérico T cumple con el tipo R. Si eso es cierto, entonces devuelve R. De lo contrario, devuelve never". Sin embargo, dado que R se infiere a partir de T y R está dentro del alcance del tipo condicional, ¿alguna vez es diferente de T? ¿No evaluaría la condición siempre a true?
En la práctica, no veo ninguna diferencia funcional entre estas definiciones de tipo (playground):
type GenericType1<T> = T extends infer R ? R : never;
type GenericType2<T> = T extends T ? T : never;
type GenericType3<T> = T;
type FooBar = {a: 'foo', b: 'bar'};
const Example1: GenericType1<FooBar> = {a: 'foo', b: 'bar'};
const Example2: GenericType2<FooBar> = {a: 'foo', b: 'bar'};
const Example3: GenericType3<FooBar> = {a: 'foo', b: 'bar'};
const Example4: FooBar = {a: 'foo', b: 'bar'};
Respuesta #1
Sí, en el ejemplo, tus declaraciones de GenericType son todas equivalentes entre sí y todas son trivialmente equivalentes al tipo T.
Podemos comprobar eso en el playground de TypeScript para confirmar:
type GenericType1<T> = T extends infer R ? R : never;
type GenericType2<T> = T extends T ? T : never;
type GenericType3<T> = T;
// all equivalent to type `number`
declare const t1: GenericType1<number>;
declare const t2: GenericType2<number>;
declare const t3: GenericType3<number>;
// would be the same as doing this:
// const t1: number;
// const t2: number;
// const t3: number;
Sin embargo, en la mayoría de los ejemplos que has enlazado, eso no es lo que está ocurriendo.
En los ejemplos que has vinculado, hay algún procesamiento adicional que ocurre después de la inferencia de tipos. (Excepto quizá tu enlace "Intentando entender los límites de...", donde la pregunta tiene un error y las partes infer U son efectivamente redundantes)
Los primeros dos enlaces que has dado en realidad están haciendo el mismo "truco" para que TypeScript colapse los tipos intermedios. Pero eso es independiente de la parte infer T de la declaración de tipo.
Aquí están las partes relevantes.
Ejemplo 1:
// Simplify<T> just makes the resulting types more readable
type Simplify<T> = T extends infer S ? {[K in keyof S]: S[K]} : never
// `infer S` isn't actually needed here and this could be written as:
// T extends any ? {[K in keyof T]: T[K]} : never
Ejemplo 2:
type ValidSelectOptionsWithKeys<K extends PropertyKey> = {
// ... complex type stuff that's not relevant to this question ...
}[K] extends infer O ? O extends any ? { [P in keyof O]: O[P] } : never : never;
// `infer O` is needed here to extract the type from the previous expression
// but then extra processing is done on the `O` type.
En ambos casos vemos el mismo patrón:
// T extends infer S ? {[K in keyof S]: S[K]} : never
// O extends any ? { [P in keyof O]: O[P] } : never
y la pista está en el comentario: Simplify<T> simplemente hace que los tipos resultantes sean más legibles
Si miras el resultado del primer ejemplo (aquí hay un enlace ejecutable al playground)
Verifica el tipo NotBoth cuando se está usando Simplify<T>:
// ✅ nice and readable
type NotBoth = {
a?: undefined;
b?: undefined;
} | {
a: string;
b?: undefined;
} | {
b: string;
a?: undefined;
}
en comparación con el mismo ejemplo sin Simplify<T> entonces puedes ver por qué está ahí:
// 🤮 really confusing and not nice
type NotBoth = NoneOf<{
a: string;
b: string;
}> | (Pick<{
a: string;
b: string;
}, "a"> & NoneOf<Omit<{
a: string;
b: string;
}, "a">>) | (Pick<{
a: string;
b: string;
}, "b"> & NoneOf<...>)
PERO ten en cuenta que la parte T extends any de la declaración es requerida.
El truco no funcionará si solo declaras:
type Simplify<T> = {[K in keyof T]: T[K]} // ❌
necesita ser algo así:
type Simplify<T> = T extends infer S ? {[K in keyof S]: S[K]} : never; // ✅
type Simplify<T> = T extends any ? {[K in keyof T]: T[K]} : never; // ✅
Así que es un poco una pista falsa: en la mayoría de tus ejemplos, la palabra clave infer se está usando correctamente (¡y de manera no trivial!) para inferir algún tipo (como en el ejemplo de la documentación de TypeScript) o los tipos inferidos tienen un procesamiento adicional realizado sobre ellos antes de ser devueltos.
