28

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 2

16

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.

Sign up to request clarification or add additional context in comments.

8 Comments

This is exactly what I'm looking for. I've updated the OP because getGraphPhoto also depends on getGraphDetails. Your answer is perfect! Thank you
Ah, sure, you can also generalize your getGraphDetails function. I'll upload a gist in a moment.
@DarkLite1 Check this out: gist.github.com/ArashMotamedi/46bc668c8f4fff4986fc7ac1e0523e8e General point is that you can make several functions generic, and pass the generic parameter (i.e. T in above examples) from one generic function to the next. getGraphDetails<T> can call callGraph<T>.
Awesome! Thank you very much. Now I have the solution and need to apply because defaultState is in another file than the Promises. I'll figure that one out. Thx again for the amazingly detailed answer.
Eric's point is valid, and you generally want to instantiate your objects (and return types from functions) based on a declared interface. But I also wanted to show you the amazing power of TS to work backwards, inferring an interface from an instance of an object or function. TypeScript offers no real type safety anyway, just development-time type hints, and the TS inference is just so delightful! But yes, for a more complex solution, please declare your interfaces first, and make sure your server and client share/agree on them.
|
10

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.

4 Comments

Setting noImplicitAny in tsconfig.json does not throw an error for that line, so it's not 100% the same logic. How would you solve this?
@DarkLite1 updated my answer, I submitted it too soon. I would expect that noImplicitAny would throw an error in other places though, places that eventually flow through to the line in question.
Thank you for the detailed answer. I also have getGraphPhoto depending on getGraphDetails, added code to the OP. In this case there's an issue setting the type on getGraphDetails and even on getGraphPhoto. I'll figure it out :)
I'll give your solution another thought, because your remarks are valid. The data does flow from the API call upwards. I just think I need to implement the type on getGraphProfile because getGraphPhoto only retuns a string. I've updated the OP to include all code.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.