-
Notifications
You must be signed in to change notification settings - Fork 32
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
docs: Add TypeScript typings to LmsApiService group methods #1426
Conversation
Codecov ReportAttention: Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## master #1426 +/- ##
==========================================
- Coverage 86.48% 86.43% -0.06%
==========================================
Files 660 660
Lines 14936 14931 -5
Branches 3164 3162 -2
==========================================
- Hits 12917 12905 -12
- Misses 1952 1956 +4
- Partials 67 70 +3 ☔ View full report in Codecov by Sentry. |
src/types.d.ts
Outdated
@@ -0,0 +1,33 @@ | |||
export type HttpResponse<Payload = {}> = { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
[suggestion] It might be worth using AxiosResponse
from the transient axios
dependency via @edx/frontend-platform
, e.g.:
import type { AxiosResponse } from 'axios';
export type EnterpriseGroupResponse = Promise<AxiosResponse<EnterpriseGroup>>;
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nice, I knew this had to be defined somewhere! Thanks for pointing it out.
src/types.d.ts
Outdated
|
||
export type EnterpriseGroup = { | ||
/* uuid of enterprise customer */ | ||
enterprise_customer: string, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
[curious/consideration] Given we typically camelCase API response data properties in service functions, e.g.:
const response = await getAuthenticatedHttpClient().get(url);
return camelCaseObject(response.data);
Should the interface represent the API response (snake_case), or how we typically consume it within hooks/components/etc. (camelCase)? For instance, when the interface is defined with snake_case, there's a greater likelihood it could discourage the pattern of camelCasing API data to make working with the data in the frontend feel more like JavaScript (camelCase is the norm in JavaScript/TypeScript).
Worth noting that looking at the resulting types within getEnterpriseGroup
on your current branch, the types are lost after the camelCaseObject
(i.e., const enterpriseGroup = camelCaseObject(response.data);
resolves to type any
).
Would it make sense to move the camelCaseObject
into the service function, and define the type with camelCase properties?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
[food for thought / aside] FWIW, while I don't think it's being used, frontend-platform
technically supports MFEs using custom middleware with axios
(ADR), e.g. integrating the http client with axios-case-converter
(docs) within the MFE's initialize
call:
initialize({
messages: [appMessages],
requireAuthenticatedUser: true,
hydrateAuthenticatedUser: true,
authMiddleware: [axiosCaseConverter],
});
See frontend-platform's implementation where the middleware function (axiosCaseConverter
in example above) is called for the client
here.
Maybe this might be an interesting topic to discuss in an upcoming Enterprise Solution Review? E.g., if we want to adopt the axios-case-converter
auth middleware to be consistent about camelCasing API responses within our Enterprise MFEs 🤔
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Overall I would agree that camelCase/snake_case conversions ideally happen in the api libraries, and so I've made that change to the group methods and adjusted their typings accordingly.
In regards to adopting an axios plugin that automatically converts snake_case to camelCase values, I would agree it's a more elegant solution, but don't know if there's a graceful way to integrate it into our code base at this point. The only way I can think to do it without modifying every point in the code that does camelCase conversion, is to carry around two axios client objects during the transition period: where one client does the automatic conversion and the legacy client does not.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Agreed, adopting it gradually would be ideal, though not the worst to tackle all existing usages in one go (N = ~65 usages). Especially given doing so would probably be less overall effort than carrying around two axios clients.
That said, if the axios plugin was enabled without any changes, the camelCaseObject
usages we have today would largely be camelCasing data that's already been camelCased, aka largely a no-op operation that doesn't necessarily break anything either (where usages could be removed incrementally, even while the axios plugin is enabled). The primary concern would be if we ever access snake_case properties prior to passing the response data to camelCaseObject
, but I don't believe we do on a cursory search through usages.
@@ -3,6 +3,22 @@ import { camelCaseObject } from '@edx/frontend-platform/utils'; | |||
|
|||
import { configuration } from '../../config'; | |||
import generateFormattedStatusUrl from './apiServiceUtils'; | |||
import { EnterpriseGroup, HttpResponse, Paginated } from '../../types'; | |||
|
|||
export type CreateEnterpriseGroupArgs = { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
[curious] I'm interested in your thoughts on when to use type
vs. interface
for object type definitions.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
IMO, types are for data specifications, while interfaces are for class/function contracts. In the case of a TypeScript project that is a web app front-end (as opposed to a library or back-end api), I don't see what I consider the ideal use cases for interfaces coming up all that often.
Also, I find the operations available to types more generally useful, especially given the heavily composition-oriented (rather than inheritance-oriented/OOP) paradigm coming from our Javascript codebase.
5c960a5
to
3c9a427
Compare
@@ -1,26 +1,8 @@ | |||
{ | |||
"extends": "@edx/typescript-config", |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nice!
src/types.d.ts
Outdated
next: string?, | ||
previous: string?, | ||
count: number, | ||
current_page: number, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit: any reason not to camelCase the entire paginated response (i.e., currentPage
) vs. only the results
list?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
None that I could see! Edited.
src/types.d.ts
Outdated
next: string?, | ||
previous: string?, | ||
count: number, | ||
current_page: number, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
[consideration] fwiw, not all paginated responses will have current_page
or start
in their response. For instance, if an API endpoint doesn't utilize DefaultPagination
from edx-drf-extensions
.
Some APIs use the base PageNumberPagination
from DRF (source):
{
"count": 1023,
"next": "https://api.example.org/accounts/?page=5",
"previous": "https://api.example.org/accounts/?page=3",
"results": [
…
]
}
[suggestion] Is it worth creating both a base paginated response and a paginated response conforming to DefaultPagination
(with current_page
and start
)?
Paginated
(base)PaginatedCurrentPage
(or similar)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Good call-out! I was not aware.
package.json
Outdated
@@ -38,6 +38,7 @@ | |||
"@edx/frontend-enterprise-utils": "9.1.0", | |||
"@edx/frontend-platform": "8.1.5", | |||
"@edx/openedx-atlas": "^0.6.0", | |||
"@edx/typescript-config": "1.1.0", |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Should @edx/typescript-config
be in devDependencies
, since TypeScript things happen during build/dev, not at runtime (e.g., in learner portal)?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Good catch, I meant to put it there!
src/data/services/LmsApiService.ts
Outdated
|
||
import { configuration } from '../../config'; | ||
import generateFormattedStatusUrl from './apiServiceUtils'; | ||
import { EnterpriseGroup, Paginated } from '../../types'; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
[nit/curious] When is it best to use the Types.*
namespace vs. explicitly including imports for the types?
Types.EnterpriseGroup
Types.Paginated
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I started out using Types.
instead of importing, but then saw that some of the deeply nested template types were losing readability. For instance:
export type EnterpriseGroupListResponse = Promise<Types.AxiosResponse<Types.Paginated<Types.EnterpriseGroup>>>;
src/types.d.ts
Outdated
|
||
export type EnterpriseGroup = { | ||
/* uuid of enterprise customer */ | ||
enterprise_customer: string, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Agreed, adopting it gradually would be ideal, though not the worst to tackle all existing usages in one go (N = ~65 usages). Especially given doing so would probably be less overall effort than carrying around two axios clients.
That said, if the axios plugin was enabled without any changes, the camelCaseObject
usages we have today would largely be camelCasing data that's already been camelCased, aka largely a no-op operation that doesn't necessarily break anything either (where usages could be removed incrementally, even while the axios plugin is enabled). The primary concern would be if we ever access snake_case properties prior to passing the response data to camelCaseObject
, but I don't believe we do on a cursory search through usages.
3c9a427
to
d6d0d3a
Compare
docs: use AxiosResponse type refactor: LmsApiService group methods convert response to camelCase build: use @edx/typescript-config compilerOptions build: move typescript-config to devDependencies docs: separate pagination types and camelCase fields build: add trailing comma to tsconfig
d6d0d3a
to
e610d9b
Compare
Jira Ticket
This change converts
LmsApiService
into a .ts file, and adds TypeScript typings to methods based around Enterprise Groups. This change is intended as a first step towards annotating all of our api contracts with TypeScript.For all changes
Only if submitting a visual change