La comprensión y el uso de inferir para el uso avanzado de mecanografiado

Maravilloso en el pasado

Prefacio

Nunca he usado inferir antes , o simplemente es returnType . No necesitas usar inferir en absoluto . Esos tutoriales en línea solo dan ejemplos y no dan escenarios específicos, por lo que la gente no puede entender bien estas cosas.

Distribución de tipos

Para inferir , es mejor hablar primero de distribución de tipos, aunque no están demasiado relacionados, pero si combina la distribución de tipos con la de inferir , la gente pensará que esta persona está en su nivel . En cuanto a conceptos como covarianza y contravarianza , es más fácil confundir a las personas y se puede dominar posteriormente.

He aprendido esto antes, pero no puedo comprender completamente el momento de su uso y no sé cómo usarlo. Por lo tanto, son dos estados en los que otros pueden entenderlo y yo mismo puedo usarlo.

Primero mire un ejemplo básico de distribución de tipos:

interface Fish {
    fish: string
}
interface Water {
    water: string
}
interface Bird {
    bird: string
}
interface Sky {
    sky: string
}
//naked type
type Condition<T> = T extends Fish ? Water : Sky;


let condition1: Condition<Fish | Bird> = { water: '水' };
let condition2: Condition<Fish | Bird> = { sky: '天空' };

Creo que este ejemplo es fácil de entender para todos, pero no sé cuándo y cómo usarlo en la práctica.

Una característica de este ejemplo es que el tipo genérico pasado en los tipos definidos en condition1 y condition2 a continuación no es el mismo que el tipo asignado posteriormente.

En otras palabras, la distribución de tipos se usa generalmente para conocer primero el tipo conocido, y el tipo del valor asignado se juzgará en función de esta distribución para deducir el tipo correspondiente.

A primera vista, parece que todavía no sirven los huevos. Por ejemplo, condition1 , conozco el tipo, solo escribo un tipo Sky | Water no es bueno? ¿Por qué los de segunda mano tienen una distribución de tipos?

El ejemplo anterior es realmente inútil, pero si se considera que lo heredado también es genérico, entonces puede eliminar rápidamente algunos tipos sin tener que redefinirlo usted mismo: (aunque muchos de estos están integrados)

type Diff<T, U> = T extends U ? never : T;


type R = Diff<"a" | "b" | "c" | "d", "a" | "c" | "f">;  // "b" | "d"


type Filter<T, U> = T extends U ? T : never;
type R1 = Filter<string | number | boolean, number>;

Dado que están integrados, no es inútil. . . Por lo tanto, debe usarse junto con inferir para ver dónde está.

Un estudio preliminar de infer

Inferir que todos deben saber que returnType se hace mediante inferir. El código es así:

type ReturnType<T extends (...args: any[]) => any> = T extends (...args: any[]) => infer R ? R : any;

A primera vista, parece un poco difícil de entender, pero de hecho, es muy fácil de entender después de una mirada más cercana. También es una distribución de tipos.

Después de aprender esto, es posible que muchas personas solo sepan que existe esta cosa, pero cuando usan Infer, no sé nada y soy así. Más tarde, cuando estaba escuchando la clase nuevamente, descubrí que esta inferir es en realidad equivalente a un marcador de posición. Use inferir X para darle un lugar, y luego combine el tipo para distribuir, puede jugar trucos. En ese momento, algunos amigos preguntaron: ¿No se puede inferir automáticamente el tipo? ¿Por qué se necesita Infer X para inferir el tipo? Joder , esta es una gran pregunta, esta es la clave para entender Infer .

Combinemos un ejemplo para ilustrar:

export {}
type Parameters<T> = T extends (...args: infer R) => any ? R : any;
type T0 = Parameters<() => string>;  // []
type T1 = Parameters<(s: string) => void>;  // [string]
type T2 = Parameters<(<T>(arg: T) => T)>;  // [unknown]

Este parámetro también está incorporado. Como puede ver, también es una distribución de tipos. La diferencia con returnType es que el marcador de posición de infer X va al parámetro para definir el tipo.

Si reemplazamos inferir R con un tipo conocido, la distribución de este tipo no es muy diferente de la demostración al principio:

type Parameters<T> = T extends (...args:string[]) => any ? string[] : any;
type T0 = Parameters<() => string>; 

Si no cambia a un tipo conocido, obtendrá un error si solo escribe R y no infiere, porque no sabe qué es R.

Entonces, ¿qué pasa si lo pasa por genéricos? Es una pena que args deba ser un tipo de matriz, por lo que las condiciones para pasarlo con genéricos deben ser limitadas:

type Parameters<T,R extends Array<any>> = T extends (...args:R) => any ? R : any;


type T0 = Parameters<() => string,string[]>; 

Se puede encontrar que esta transmisión no es muy diferente de la transmisión de tipo conocido, porque cuando se pasa el segundo tipo genérico, conocemos este tipo, por lo que en este caso, no es muy útil, a menos que se pase el tipo genérico. Es otra persona, así que cuando escribimos esta biblioteca, podemos obtener tipos definidos por el usuario. En este momento, es útil.

De esta manera, puede encontrar que inferir puede ocupar cualquier posición en la derivación de tipo, y el tipo de derivación final puede usar el tipo requerido en el medio. Puede mirar este ejemplo para profundizar su comprensión:

  type T1 = { name: string };
  type T2 = { age: number };
  
  type UnionToInterp<T> = T extends { a: (x: infer U) => void; b: (x: infer U) => void } ? U : never;
  type T3 = UnionToInterp<{ a: (x: T1) => void; b: (x: T2) => void }>; // T1 & T2

Este ejemplo es que infer obtiene los parámetros, los parámetros de las dos funciones, por qué dos saldrán del tipo cruzado, aquí está la covarianza, entonces es el tipo cruzado.

Veamos un ejemplo más difícil, que proviene del reclutamiento de Leetcode:

https://github.com/LeetCode-OpenSource/hire/blob/master/typescript_zh.md

El título es este:

interface Action<T> {
    payload?: T;
    type: string;
  }
  
  class EffectModule {
    count = 1;
    message = "hello!";
  
    delay(input: Promise<number>) {
      return input.then(i => ({
        payload: `hello ${i}!`,
        type: 'delay'
      }));
    }
  
    setMessage(action: Action<Date>) {
      return {
        payload: action.payload!.getMilliseconds(),
        type: "set-message"
      };
    }
  }
  
  // 修改 Connect 的类型,让 connected 的类型变成预期的类型
  type Connect = (module: EffectModule) => any;
  
  const connect: Connect = m => ({
    delay: (input: number) => ({
      type: 'delay',
      payload: `hello 2`
    }),
    setMessage: (input: Date) => ({
      type: "set-message",
      payload: input.getMilliseconds()
    })
  });
  
  type Connected = {
    delay(input: number): Action<string>;
    setMessage(action: Date): Action<number>;
  };
  
  export const connected: Connected = connect(new EffectModule());

Se requiere modificar any para devolver el tipo correcto, y este tipo debe ser el mismo que conectado.

Algunos estudiantes dijeron, simplemente cambie "cualquiera" a "conectado". Si fuera tan simple, esta pregunta no se plantearía. Esto definitivamente es para que lo inicies, y el tipo de este conectado es un método en la instancia de EffectModule, y los parámetros y los retornos internos se han modificado.

Cómo hacer esta pregunta, primero paso a paso, primero extraiga el método effectModule, de lo contrario no hay un siguiente paso.

No hay un método listo para extraer la clase, y no debe ser clave de EffectModule, porque hay otras cosas, ¿cómo excluir otras cosas? Es usar distribución de tipos y la clase puede tomar valores. Si es una función, entonces extráigala, de lo contrario no extraerá:

  type MethodName<T> = {[F in keyof T]:T[F] extends Function ? F:never}[keyof T]  type EE =  MethodName<EffectModule>

Al mismo tiempo, si el valor es nunca, la clave de no volverá. Este párrafo es bastante esclarecedor, porque muchas veces quiero hacer un tipo de juicio circular y luego hacer una selección. Este es un buen ejemplo.

Después de obtener el nombre, para modificar el método, necesita:

asyncMethod<T, U>(input: Promise<T>): Promise<Action<U>> asyncMethod<T, U>(input: T): Action<U> syncMethod<T, U>(action: Action<T>): Action<U>  syncMethod<T, U>(action: T): Action<U>

Esto viene dado por el título, cópielo directamente:

type asyncMethod<T, U> = (input: Promise<T>) => Promise<Action<U>> type asyncMethodConnect<T, U> = (input: T) => Action<U> type syncMethod<T, U> = (action: Action<T>) => Action<U> type syncMethodConnect<T, U> = (action: T) => Action<U> 

Luego, debe hacer una distribución de tipos para determinar qué método es y luego distribuir a qué método:

type EffectMethodAssign<T> = T extends asyncMethod<infer U, infer V>      ? asyncMethodConnect<U, V>     : T extends syncMethod<infer U, infer V>     ? syncMethodConnect<U, V>     : never

Este párrafo es muy simple, es decir, el juicio de distribución, el tipo genérico está bien con inferir .

Finalmente, modifique Connect y listo.

  type Connect = (module: EffectModule) => {
      [F in MethodName<typeof module>]:EffectMethodAssign<typeof module[F]>
  }

Supongo que te gusta

Origin blog.csdn.net/KlausLily/article/details/108878205
Recomendado
Clasificación