@typescript-eslint/no-unsafe-assignment: Unsafe assignment of an any value

Viewed 58622

Consider the following code:

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 }
}

Which generates the ESLint warning:

@typescript-eslint/no-unsafe-assignment: Unsafe assignment of an any value.

This means that the properties in response.data might not match the ones of the profile. The return of getGraphProfile is Promise<AxiosResponse<any>>. Of course it's easy to get rid of this ESLint warning by simply ignoring it:

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

Questions:

  • How is it possible to shape the data in the Promise getGraphProfile so it does match? Because one can create a TS interface but that would simply create duplicate code with the object defaultState().profile
  • Why is TypeScript not having an issue with this code but the linter does? Do both not need to be alligned?

The implementations:

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}`)
  }
}
2 Answers

TypeScript doesn't generate warnings, only errors. As far as TS is concerned, that any assignment is valid. This is where the linter comes in to offer additional support.

Luckily you don't need to duplicate your interface. Use TypeScript's ReturnType to get the type of the profile object in your defaultState method:

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

The above line utilizes 3 great TypeScript features:

  • ReturnType to infer the type that a function returns
  • typeof to infer the interface from an object instance
  • ["profile"] to get the type of a certain property of an interface

Now, make your callGraph function generic:

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 })
}

And update the callGraph call in your getGraphDetails function:

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

Now your graph calls are properly typed, and you didn't have to duplicate your profile definition; rather you used TypeScript's awesome type inference technique to "read your interface" from the return type of your function.

Going to answer your questions in reverse order:

Why is TypeScript not having an issue with this code but the linter does? Do both not need to be alligned?

In Typescript, something with type any can be assigned to anything. Using any essentially removes typesafety from that part of the code. For example:

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

I guess the point of that eslint rule is to catch places where you might not be wanting to actually assign something with type any to something else. To be honest, I'm not quite sure why one would use that linting rule given that the compiler option noImplicitAny exists.

How is it possible to shape the data in the Promise getGraphProfile so it does match? Because one can create a TS interface but that would simply create duplicate code with the object defaultState().profile

There are a few ways you could solve this. The simplest approach would probably be to type the return value of getGraphDetails:

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

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

But usually it's better to type the data at as low a level as possible, which in this case means the callGraph function:

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 })
}

By doing it that way, now callGraph's return value is typed, and TS will therefore know that getGraphDetails and getGraphProfile both return that same type, since they ultimately just pass through the API response.

Last option: I don't use Axios, but I bet its Typescript definition would let you do this:

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

I have removed the Promise<GraphDetailsPayload> return type, and have instead just "passed in" the GraphDetailsPayload type via the angle brackets to the axios function call. This is making use of something called "generics", which are the most fun and complex part of typesystems like TS. You'll encounter them a lot in libraries you use, and you'll eventually start writing functions that take generics as well.

Related