Upgrade to Pro — share decks privately, control downloads, hide ads and more …

TypescriptCodegen.pdf

Ovidiu Latcu
November 17, 2023
28

 TypescriptCodegen.pdf

Ovidiu Latcu

November 17, 2023
Tweet

Transcript

  1. Agenda • Why Typescript ? • Fetching external (remote) data

    • The problem • Open-API • GraphQL • Other alternatives
  2. Why Typescript The problem with Javascript function getUser() { return

    { id: 1, first: "John", last: "Doe", email: "[email protected]", }; } function greetUser() { const user = getUser(); console.log(`Hello ${user.first} ${user.last}`); }
  3. Why Typescript The problem with Javascript function getUser() { return

    { id: 1, firstName: "John", lastName: "Doe", email: "[email protected]", }; } function greetUser() { const user = getUser(); console.log(`Hello ${user.firstName} ${user.lastName}`); }
  4. Why Typescript The problem with Javascript function emailOrderToUser(orderId) { const

    user = getUser(); sendEmail( `Thank you ${user.first} ${user.last} for placing your order with us!` ); }
  5. Why Typescript The problem with Javascript function emailOrderToUser(orderId) { const

    user = getUser(); sendEmail( `Thank you ${user.first} ${user.last} for placing your order with us!` ); } Our refactor broke this! And there is now compiler in JS to warn us.
  6. Why Typescript The problem with Javascript function getUser(){ return {

    id: 1, firstName: "John", lastName: "Doe", email: "[email protected]", }; }
  7. Why Typescript The problem with Javascript type User = {

    id: number; firstName: string; lastName: string; email: string; }; function getUser(): User { return { id: 1, firstName: "John", lastName: "Doe", email: "[email protected]", }; }
  8. Why Typescript The problem with Javascript type User = {

    id: number; firstName: string; lastName: string; email: string; }; function getUser(): User { return { id: 1, firstName: "John", lastName: "Doe", email: "[email protected]", }; } function emailOrderToUser(orderId: number) { const user = getUser(); sendEmail( `Thank you ${user.first} ${user.last} for placing your order with us!` ); }
  9. Why Typescript The problem with Javascript type User = {

    id: number; firstName: string; lastName: string; email: string; }; function getUser(): User { return { id: 1, firstName: "John", lastName: "Doe", email: "[email protected]", }; } function emailOrderToUser(orderId: number) { const user = getUser(); sendEmail( `Thank you ${user.firstName} ${user.lastName} for placing your order with us!` ); }
  10. Fetching external data async function fetchUserData() { const response =

    await fetch(API_URL + "/userinfo"); const userInfo = await response.json(); }
  11. Fetching external data async function fetchUserData() { const response =

    await fetch(API_URL + "/userinfo"); const userInfo = await response.json(); // ^? const userInfo: any } 🫠
  12. Fetching external data async function fetchUserData() { const response =

    await fetch(API_URL + "/userinfo"); const userInfo: User = await response.json(); } type User = { id: number; email: string; };
  13. Fetching external data async function fetchUserData() { const response =

    await fetch(API_URL + "/userinfo"); const userInfo: User = await response.json(); } type User = { id: number; email: string; }; ❗❗We are casting ‘any’ to ‘User’ This has no type safety guarantee
  14. Fetching external data async function fetchUserData() { const response =

    await fetch(API_URL + "/userinfo"); const userInfo: User = await response.json(); } type User = { id: number; email: string; }; ❗❗We are casting ‘any’ to ‘User’ This has no type safety guarantee If the API changes this will not fail or warn us in any way. ⚠
  15. Fetching external data async function fetchUserData() { const response =

    await fetch(API_URL + "/userinfo"); const userInfo: User = await response.json(); } type User = { id: number; email: string; }; ❗❗We are casting ‘any’ to ‘User’ This has no type safety guarantee If the API changes this will not fail or warn us in any way. ⚠ We must update/maintain our types every time API changes ⚠
  16. Fetching external data async function fetchUserData() { const response =

    await fetch(API_URL + "/userinfo"); const userInfo: User = await response.json(); } const UserSchema = z.object({ id: z.number(), email: z.string(), }); type User = z.infer<typeof UserSchema>;
  17. Fetching external data async function fetchUserData() { const response =

    await fetch(API_URL + "/userinfo"); const userInfo: User = UserSchema.parse(await response.json()); } const UserSchema = z.object({ id: z.number(), email: z.string(), }); type User = z.infer<typeof UserSchema>;
  18. Fetching external data async function fetchUserData() { const response =

    await fetch(API_URL + "/userinfo"); const userInfo: User = UserSchema.parse(await response.json()); } const UserSchema = z.object({ id: z.number(), email: z.string(), }); type User = z.infer<typeof UserSchema>; This parsing will fail if API changes. It ensure we are receiving the correct type. Better. ✅
  19. Fetching external data async function fetchUserData() { const response =

    await fetch(API_URL + "/userinfo"); const userInfo: User = UserSchema.parse(await response.json()); } const UserSchema = z.object({ id: z.number(), email: z.string(), }); type User = z.infer<typeof UserSchema>; This parsing will fail if API changes. It ensure we are receiving the correct type. Better. ✅ We still need to manually update/maintain our schema ⚠
  20. Issues with this approach • When API changes, somebody has

    to inform us to update our types • We need to manually update our types each time API changes • Manual work is involved, leaving room for errors
  21. Ideal setup • We automatically detect when API has changed

    • We automatically generate client types based on API schema • Everything is done automatically, no manual work involved
  22. OpenAPI Specification • Formerly known as the Swagger Speci fi

    cation • Speci fi cation de fi ning the API request/response formats • JSON or YAML • Serves as an API Documentation • Tools and code generators for client SDKs
  23. OpenAPI Specification Example components: schemas: User: type: object required: -

    id - email properties: id: type: number example: 1 firstName: type: string example: John lastName: type: string example: Doe email: type: string format: email example: [email protected]
  24. OpenAPI Specification Example paths: /userinfo: get: summary: Get user information

    description: Retrieves details about a user. responses: "200": description: Successful response content: application/json: schema: $ref: "#/components/schemas/User" components: schemas: User: type: object required: - id - email properties: id: type: number example: 1 firstName: type: string example: John lastName: type: string example: Doe email: type: string format: email example: [email protected]
  25. OpenAPI specifications Creating an OpenAPI YAML or JSON • Manual

    adding and editing the spec fi le • Online API design tools (StopLight, Insomnia, PostMan) • Generate from code annotations
  26. Code annotations Javascript/Typescript /** * @openapi * /api/userinfo: * get:

    * summary: Get user information * description: Retrieves details about a user. * responses: * '200': * description: Successful response * content: * application/json: * schema: * $ref: '#/components/schemas/User' */ export async function GET(request: Request) { return Response.json({ id: 1, firstName: "John", lastName: "Doe", email: "[email protected]", }); }
  27. Code annotations Java import io.swagger.annotations.Api; import io.swagger.annotations.ApiOperation; import io.swagger.annotations.ApiResponse; import

    io.swagger.annotations.ApiResponses; @Api(tags = "User Operations") @RestController @RequestMapping("/api") public class UserController { @ApiOperation(value = "Get user information", notes = "Retrieves details about a user.") @ApiResponses({ @ApiResponse(code = 200, message = "Successful response", response = User.class), @ApiResponse(code = 404, message = "User not found", response = ErrorResponse.class) }) @GetMapping("/userinfo") public ResponseEntity<User> getUserInfo() { User user = new User(); user.setId(1L); user.setFirstName("John"); user.setLastName("Doe"); user.setEmail("[email protected]"); return ResponseEntity.ok(user); } }
  28. OpenAPI Generating client SDK / Types • We have generated

    our Open API speci fi cation • We can also generate client SDK or types • Language agnostic ( YAML -> Java, Typescript, Kotlin, Swift, GO, etc ) • Multiple open source tools to facilitate this • We get typed models and endpoints • Automatic code generation • Saves a lot of development time
  29. OpenAPI client SDK generation Sd Open API Spec JSON /

    YAML Sd Client SDK / Types Typescript Kotlin Swift
  30. OpenAPI client SDK generation components: schemas: User: type: object required:

    - id - email properties: id: type: number example: 1 firstName: type: string example: John lastName: type: string example: Doe email: type: string format: email example: [email protected] export interface components { schemas: { User: { /** @example 1 */ id: number; /** @example John */ firstName?: string; /** @example Doe */ lastName?: string; /** * Format: email * @example [email protected] */ email: string; }; //… }; export type User = components['schemas']['User']
  31. Fetching external data async function fetchUserData() { const response =

    await fetch(API_URL + "/userinfo"); const userInfo: User = await response.json(); } type User = { id: number; email: string; firstName: string; lastName: string; };
  32. Fetching external data async function fetchUserData() { const response =

    await fetch(API_URL + "/userinfo"); const userInfo: User = await response.json(); } type User = { id: number; email: string; firstName: string; lastName: string; }; //generated schema.d.ts from OpenAPI specs export interface components { schemas: { User: { id: number; firstName?: string; lastName?: string; /** * Format: email * @example [email protected] */ email: string; }; }; } export interface paths { "/api/userinfo": { /** * Get user information * @description Retrieves details about a user. */ get: { responses: { /** @description Successful response */ 200: { content: { "application/json": components["schemas"] ["User"]; }; }; }; }; }; }
  33. Fetching external data async function fetchUserData() { const response =

    await fetch(API_URL + "/userinfo"); const userInfo: User = await response.json(); } type User = { id: number; email: string; firstName: string; lastName: string; }; //generated schema.d.ts from OpenAPI specs export interface components { schemas: { User: { id: number; firstName?: string; lastName?: string; /** * Format: email * @example [email protected] */ email: string; }; }; } export interface paths { "/api/userinfo": { /** * Get user information * @description Retrieves details about a user. */ get: { responses: { /** @description Successful response */ 200: { content: { "application/json": components["schemas"] ["User"]; }; }; }; }; }; } We no longer need to maintain our own type ✅
  34. Fetching external data import { components } from "../dist/schema"; async

    function fetchUserData() { const response = await fetch(API_URL + "/userinfo"); const userInfo: User = await response.json(); } type User = components["schemas"]["User"]; //generated schema.d.ts from OpenAPI specs export interface components { schemas: { User: { id: number; firstName?: string; lastName?: string; /** * Format: email * @example [email protected] */ email: string; }; }; } export interface paths { "/api/userinfo": { /** * Get user information * @description Retrieves details about a user. */ get: { responses: { /** @description Successful response */ 200: { content: { "application/json": components["schemas"] ["User"]; }; }; }; }; }; } We no longer need to maintain our own type ✅
  35. Fetching external data import { components } from "../dist/schema"; async

    function fetchUserData() { const response = await fetch(API_URL + "/userinfo"); const userInfo: User = await response.json(); } type User = components["schemas"]["User"]; //generated schema.d.ts from OpenAPI specs export interface components { schemas: { User: { id: number; firstName?: string; lastName?: string; /** * Format: email * @example [email protected] */ email: string; }; }; } export interface paths { "/api/userinfo": { /** * Get user information * @description Retrieves details about a user. */ get: { responses: { /** @description Successful response */ 200: { content: { "application/json": components["schemas"] ["User"]; }; }; }; }; }; } We no longer need to maintain our own type ✅ We import types from our generated types ✅
  36. Fetching external data import { components } from "../dist/schema"; async

    function fetchUserData() { const response = await fetch(API_URL + "/userinfo"); const userInfo: User = await response.json(); } type User = components["schemas"]["User"]; //generated schema.d.ts from OpenAPI specs export interface components { schemas: { User: { id: number; firstName?: string; lastName?: string; /** * Format: email * @example [email protected] */ email: string; }; }; } export interface paths { "/api/userinfo": { /** * Get user information * @description Retrieves details about a user. */ get: { responses: { /** @description Successful response */ 200: { content: { "application/json": components["schemas"] ["User"]; }; }; }; }; }; } We no longer need to maintain our own type ✅ We import types from our generated types ✅ We easily keep types in SYNC ✅
  37. Open API Specification Advantages • Easy way of documenting your

    BE APIs • Generated Swagger API documentation • Generated client SDK / types • Client and BE use the same types / de fi nitions • Many things can be automated and generated • Systems using up to date types, making everything more robust and safe
  38. GraphQL • Query Language for APIs • Declarative data fetching

    • Allows clients to request speci fi c data • Helps avoid fetching un-necessary data • Helps avoid multiple fetches
  39. GraphQL Schema • Schema Driven development • GraphQL APIs driven

    by a strongly typed schema • Schema de fi nes data types • Schema lists available operations / queries • Serves as a contract between client and server • Promotes easier collaboration and keeping data types in sync
  40. GraphQL Schema de fi nition language type User { id:

    Float! firstName: String lastName: String email: String! } type Query { getUserInfo: User }
  41. GraphQL Schema de fi nition language type User { id:

    Float! firstName: String lastName: String email: String! friends: [User] } type Query { getUserInfo: User getFriendsSuggestions: [User] }
  42. GraphQL Schema de fi nition language type User { id:

    Float! firstName: String lastName: String email: String! friends: [User] } type Query { getUserInfo: User getFriendsSuggestions: [User] } type Mutation { addFriend(userId: Float!): User }
  43. GraphQL Writing a query type User { id: Float! firstName:

    String lastName: String email: String! friends: [User] } type Query { getUserInfo: User getFriendsSuggestions: [User] } type Mutation { addFriend(userId: Float!): User } query GetUserInfo { } Name the query
  44. GraphQL Writing a query type User { id: Float! firstName:

    String lastName: String email: String! friends: [User] } type Query { getUserInfo: User getFriendsSuggestions: [User] } type Mutation { addFriend(userId: Float!): User } query GetUserInfo { getUserInfo } Name the query Choose the operation
  45. GraphQL Writing a query type User { id: Float! firstName:

    String lastName: String email: String! friends: [User] } type Query { getUserInfo: User getFriendsSuggestions: [User] } type Mutation { addFriend(userId: Float!): User } query GetUserInfo { getUserInfo { id email firstName lastName } } Select the desired fi elds { }
  46. GraphQL Writing a query type User { id: Float! firstName:

    String lastName: String email: String! friends: [User] } type Query { getUserInfo: User getFriendsSuggestions: [User] } type Mutation { addFriend(userId: Float!): User } query GetUserInfo { getUserInfo { id email firstName lastName } } Select the desired fi elds { } Avoiding over-fetching
  47. GraphQL Writing a query in Typescript const UserInfoQuery = gql`

    query GetUserInfo { getUserInfo { id email firstName lastName } } `;
  48. GraphQL Writing a query in Typescript const UserInfoQuery = gql`

    query GetUserInfo { getUserInfo { id email firstName lastName } } `; const fetchUserInfo = async () => { const response = await client.executeQuery( createRequest(UserInfoQuery, {}) ); const body = response.data; };
  49. GraphQL Writing a query in Typescript const UserInfoQuery = gql`

    query GetUserInfo { getUserInfo { id email firstName lastName } } `; const fetchUserInfo = async () => { const response = await client.executeQuery( createRequest(UserInfoQuery, {}) ); const body = response.data; }; any 🫠
  50. GraphQL Writing a query in Typescript const UserInfoQuery = gql`

    query GetUserInfo { getUserInfo { id email firstName lastName } } `; const fetchUserInfo = async () => { const response = await client.executeQuery<UserInfoResponse>( createRequest(UserInfoQuery, {}) ); const body = response.data; };
  51. GraphQL Writing a query in Typescript const UserInfoQuery = gql`

    … `; const fetchUserInfo = async () => { const response = await client.executeQuery<UserInfoResponse>( createRequest(UserInfoQuery, {}) ); const body = response.data; };
  52. GraphQL Writing a query in Typescript const UserInfoQuery = gql`

    … `; type UserInfoResponse = { id: string; email: string; firstName: string; lastName: string; }; const fetchUserInfo = async () => { const response = await client.executeQuery<UserInfoResponse>( createRequest(UserInfoQuery, {}) ); const body = response.data; };
  53. GraphQL Writing a query in Typescript const UserInfoQuery = gql`

    query GetUserInfo { getUserInfo { id email firstName lastName } } `; type UserInfoResponse = { id: string; email: string; firstName: string; lastName: string; };
  54. GraphQL Writing a query in Typescript const UserInfoQuery = gql`

    query GetUserInfo { getUserInfo { id email firstName lastName } } `; type UserInfoResponse = { id: string; email: string; firstName: string; lastName: string; }; We are again manually de fi ning our types 🫠
  55. GraphQL Writing a query in Typescript const UserInfoQuery = gql`

    query GetUserInfo { getUserInfo { id email firstName lastName } } `; type UserInfoResponse = { id: string; email: string; firstName: string; lastName: string; }; We are again manually de fi ning our types 🫠 We must keep properties names and types in sync with GQL schema
  56. GraphQL Code generator • A Tool that generates code out

    of your GraphQL schema • Generates types • Generates operations • Plugins : React, Vue, Typescript, C#
  57. GraphQL code generator Advantages • Eliminates a lot of mistakes

    • Types kept in sync with schema • Type safe access to response objects • Speeds up development
  58. GraphQL Codegenerator //codegen.ts const config: CodegenConfig = { overwrite: true,

    schema: "examples/schema.graphql", documents: "**/*.tsx", generates: { "lib/apis/": { preset: "client", plugins: [], }, "./graphql.schema.json": { plugins: ["introspection"], }, }, }; $ yarn graphql-codegen —con fi g codegen.ts
  59. GraphQL Codegenerator //Generated types file //… export type GetUserInfoQuery =

    { __typename?: "Query"; getUserInfo: { __typename?: "User"; id: number; email: string; firstName?: string | null; lastName?: string | null; }; }; //…
  60. GraphQL Codegenerator const UserInfoQuery = graphql(` query GetUserInfo { getUserInfo

    { id email firstName lastName } } `); const fetchUserInfo = async () => { const response = await client.executeQuery<GetUserInfoQuery>( createRequest(UserInfoQuery, {}) ); const body = response.data; }; //Generated types file //… export type GetUserInfoQuery = { __typename?: "Query"; getUserInfo: { __typename?: "User"; id: number; email: string; firstName?: string | null; lastName?: string | null; }; }; //…
  61. GraphQL Codegenerator const UserInfoQuery = graphql(` query GetUserInfo { getUserInfo

    { id email firstName lastName } } `); const fetchUserInfo = async () => { const response = await client.executeQuery<GetUserInfoQuery>( createRequest(UserInfoQuery, {}) ); const body = response.data; }; //Generated types file //… export type GetUserInfoQuery = { __typename?: "Query"; getUserInfo: { __typename?: "User"; id: number; email: string; firstName?: string | null; lastName?: string | null; }; }; //…
  62. GraphQL Codegenerator const UserInfoQuery = graphql(` query GetUserInfo { getUserInfo

    { id email firstName lastName } } `); const fetchUserInfo = async () => { const response = await client.executeQuery<GetUserInfoQuery>( createRequest(UserInfoQuery, {}) ); const body = response.data; }; //Generated types file //… export type GetUserInfoQuery = { __typename?: "Query"; getUserInfo: { __typename?: "User"; id: number; email: string; firstName?: string | null; lastName?: string | null; }; }; //… } {
  63. Code generation • Are you doing a lot of manual

    work ? • Is external APIs schema hard to determine ? • Is it a problem to maintain types ? • Is it hard to identify changes in external types ? • Maybe this can be automated via code generation
  64. Headless CMS • Popular Headless CMS • Types and models

    are de fi ned in a GUI • All come with code generation solutions • Allows us to bring those types to client code
  65. Databases and ORMs • Many available solutions • Generate Types

    from DBSchema • Generate type safe client SDKs • Typesafety in mind Drizzle ORM