Búsqueda de sitios web

javascript - @typescript-eslint/no-unsafe-assignment: Asignación insegura de un valor any


Considera el siguiente código:

const defaultState = () => {
  return {
    profile: {
      id: '',
      displayName: '',
      givenName: '',
    },
    photo: '',
  }
}

const state = reactive(defaultState())

export const setGraphProfile = async () => {
  const response = await getGraphProfile()
  state.profile = { ...defaultState().profile, ...response.data }
}

Lo cual genera la advertencia de ESLint:

@typescript-eslint/no-unsafe-assignment: Asignación insegura de un valor de tipo any.

Esto significa que las propiedades en response.data podrían no coincidir con las del profile. El retorno de getGraphProfile es Promise<AxiosResponse<any>>. Por supuesto, es fácil deshacerse de esta advertencia de ESLint simplemente ignorándola:

// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
state.profile = { ...defaultState().profile, ...response.data }

Preguntas:

  • ¿Cómo es posible dar forma a los datos en la Promesa getGraphProfile para que coincidan? Porque uno puede crear una interface de TS, pero eso simplemente crearía código duplicado con el objeto defaultState().profile
  • ¿Por qué TypeScript no tiene problemas con este código, pero el linter sí? ¿No necesitan estar alineados ambos?

Las implementaciones:

const callGraph = (
  url: string,
  token: string,
  axiosConfig?: AxiosRequestConfig
) => {
  const params: AxiosRequestConfig = {
    method: 'GET',
    url: url,
    headers: { Authorization: `Bearer ${token}` },
  }
  return axios({ ...params, ...axiosConfig })
}

const getGraphDetails = async (
  uri: string,
  scopes: string[],
  axiosConfig?: AxiosRequestConfig
) => {
  try {
    const response = await getToken(scopes)
    if (response && response.accessToken) {
      return callGraph(uri, response.accessToken, axiosConfig)
    } else {
      throw new Error('We could not get a token because of page redirect')
    }
  } catch (error) {
    throw new Error(`We could not get a token: ${error}`)
  }
}

export const getGraphProfile = async () => {
  try {
    return await getGraphDetails(
      config.resources.msGraphProfile.uri,
      config.resources.msGraphProfile.scopes
    )
  } catch (error) {
    throw new Error(`Failed retrieving the graph profile: ${error}`)
  }
}

export const getGraphPhoto = async () => {
  try {
    const response = await getGraphDetails(
      config.resources.msGraphPhoto.uri,
      config.resources.msGraphPhoto.scopes,
      { responseType: 'arraybuffer' }
    )
    if (!(response && response.data)) {
      return ''
    }
    const imageBase64 = new Buffer(response.data, 'binary').toString('base64')
    // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
    return `data:${response.headers['content-type']};base64, ${imageBase64}`
  } catch (error) {
    throw new Error(`Failed retrieving the graph photo: ${error}`)
  }
}

Respuesta #1

TypeScript no genera advertencias, solo errores. En lo que respecta a TS, esa asignación any es válida. Aquí es donde entra el linter para ofrecer soporte adicional.

Por suerte, no necesitas duplicar tu interfaz. Usa ReturnType de TypeScript para obtener el tipo del objeto profile en tu método defaultState:

type IProfile = ReturnType<typeof defaultState>["profile"]

La línea anterior utiliza 3 grandes características de TypeScript:

  • ReturnType para inferir el tipo que devuelve una función
  • Usar typeof para inferir la interfaz a partir de una instancia de objeto
  • ["profile"] para obtener el tipo de una determinada propiedad de una interfaz

Ahora, haz que tu función callGraph sea genérica:

function callGraph<T>(url: string, token: string, axiosConfig?: AxiosRequestConfig) {
  const params: AxiosRequestConfig = {
    method: 'GET',
    url: url,
    headers: { Authorization: `Bearer ${token}` },
  }
  return axios.request<T>({ ...params, ...axiosConfig })
}

Y actualiza la llamada callGraph en tu función getGraphDetails:

...
  if (response && response.accessToken) {
    return callGraph<IProfile>(uri, response.accessToken, axiosConfig)
  }
...

Ahora tus llamadas al gráfico están correctamente tipadas, y no tuviste que duplicar la definición de tu perfil; más bien, utilizaste la increíble técnica de inferencia de tipos de TypeScript para "leer tu interfaz" a partir del tipo de retorno de tu función.

Respuesta #2

Voy a responder tus preguntas en orden inverso:

¿Por qué TypeScript no tiene problemas con este código, pero el linter sí? ¿No necesitan estar alineados ambos?

En Typescript, algo con el tipo any puede ser asignado a cualquier cosa. Usar any esencialmente elimina la seguridad de tipos de esa parte del código. Por ejemplo:

const foo: number = 'hello' as any // Typescript is fine with this

Supongo que el objetivo de esa regla de eslint es detectar lugares donde podrías no querer realmente asignar algo con tipo any a otra cosa. Para ser honesto, no estoy muy seguro de por qué alguien usaría esa regla de lint dado que existe la opción del compilador noImplicitAny.

¿Cómo es posible dar forma a los datos en la Promise getGraphProfile para que coincidan? Porque uno puede crear una interfaz TS, pero eso simplemente crearía código duplicado con el objeto defaultState().profile

Hay algunas formas en que podrías resolver esto. El enfoque más sencillo probablemente sería escribir el valor de retorno de getGraphDetails:

type GraphDetailsPayload = {
  id: string,
  displayName: string,
  givenName: string,
}

export const getGraphProfile = async (): Promise<GraphDetailsPayload> => {
  ...
}

Pero normalmente es mejor escribir los datos al nivel más bajo posible, lo que en este caso significa la función callGraph:

const callGraph = (
  url: string,
  token: string,
  axiosConfig?: AxiosRequestConfig
): Promise<GraphDetailsPayload> => {
  const params: AxiosRequestConfig = {
    method: 'GET',
    url: url,
    headers: { Authorization: `Bearer ${token}` },
  }
  return axios({ ...params, ...axiosConfig })
}

Haciéndolo de esa manera, ahora el valor de retorno de callGraph tiene tipo, y por lo tanto TS sabrá que getGraphDetails y getGraphProfile devuelven ambos ese mismo tipo, ya que en última instancia solo pasan a través de la respuesta de la API.

Última opción: no uso Axios, pero apuesto a que su definición en TypeScript te permitiría hacer esto:

const callGraph = (
  url: string,
  token: string,
  axiosConfig?: AxiosRequestConfig
) => {
  const params: AxiosRequestConfig = {
    method: 'GET',
    url: url,
    headers: { Authorization: `Bearer ${token}` },
  }
  return axios<GraphDetailsPayload>({ ...params, ...axiosConfig })
}

He eliminado el tipo de retorno Promise<GraphDetailsPayload>, y en su lugar simplemente he "pasado" el tipo GraphDetailsPayload a través de los corchetes angulares a la llamada de función axios. Esto hace uso de algo llamado "genéricos", que son la parte más divertida y compleja de los sistemas de tipos como TS. Te encontrarás con ellos frecuentemente en las librerías que uses, y eventualmente comenzarás a escribir funciones que también reciban genéricos.

Artículos relacionados