Last active
June 29, 2021 14:00
-
-
Save kuroski/99d2d158d4365f6a899df44ae1704166 to your computer and use it in GitHub Desktop.
Confident JS series
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// utils.d.ts | |
type CamelCase<S extends string> = S extends `${infer P1}_${infer P2}${infer P3}` | |
? `${Lowercase<P1>}${Uppercase<P2>}${CamelCase<P3>}` | |
: Lowercase<S> | |
type CamelToSnakeCase<S extends string> = S extends `${infer T}${infer U}` ? | |
`${T extends Capitalize<T> ? "_" : ""}${Lowercase<T>}${CamelToSnakeCase<U>}` : | |
S | |
export type KeysToCamelCase<T> = { | |
[K in keyof T as CamelCase<string & K>]: T[K] extends {} ? KeysToCamelCase<T[K]> : T[K] | |
} | |
export type KeysToSnakeCase<T> = { | |
[K in keyof T as CamelToSnakeCase<string & K>]: T[K] extends {} ? KeysToSnakeCase<T[K]> : T[K] | |
} | |
// utils.js | |
// @ts-check | |
import humps from "humps" | |
/** @type {<T>(obj: T) => import(".").KeysToSnakeCase<T>} */ | |
export const objectKeysToSnakeCase = humps.decamelizeKeys | |
/** @type {<T>(obj: T) => import(".").KeysToCamelCase<T>} */ | |
export const objectKeysToCamelCase = humps.camelizeKeys |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// humps.d.ts -> the name must match the library in this case | |
declare module "humps" { | |
function decamelizeKeys<T>(obj: T): import("./index").KeysToSnakeCase<T>; | |
function camelizeKeys<T>(obj: T): import("./index").KeysToCamelCase<T>; | |
} | |
// utils.js | |
// @ts-check | |
import humps from "humps"; | |
export const objectKeysToSnakeCase = humps.decamelizeKeys; | |
export const objectKeysToCamelCase = humps.camelizeKeys; |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import * as d from "decoders" | |
const ProductDecoder = d.object({ | |
price: d.number, | |
vat: d.number | |
}) | |
const ProductGuard = d.guard(ProductDecoder) | |
const rawData = JSON.stringify({ | |
price: "10", | |
vat: 1.5 | |
}) | |
const api = () => Promise.resolve(JSON.parse(rawData)).then(ProductGuard) | |
function init() { | |
api().then((result) => { | |
console.log(`Total price is ${result.price + result.vat}`) | |
}) | |
} | |
init() |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import * as d from "decoders" | |
type Product = { | |
price: number | |
vat: number | |
} | |
const ProductDecoder: d.Decoder<Product> = d.object({ | |
price: d.number, | |
vat: d.number | |
}) | |
const ProductGuard: d.Guard<Product> = d.guard(ProductDecoder) | |
const rawData: string = JSON.stringify({ | |
price: "10", | |
vat: "1,5" | |
}) | |
const api = (): Promise<Product> => Promise.resolve(JSON.parse(rawData)) | |
const calculateTotalPrice = (product: Product): number => product.price + product.vat | |
function init() { | |
api().then((result) => { | |
console.log(`Total price is ${calculateTotalPrice(result)}`) | |
}) | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import * as d from "decoders" | |
import objectKeysToSnakeCase from "./utils" | |
// It might seem redundant since we have the same structure in the decoder | |
// but some times, what you are sending to an API have a different data structure | |
const ProductEncoder = d.object({ | |
price: d.number, | |
vat: d.number | |
}) | |
const UserEncoder = d.object({ | |
firstName: d.string, | |
lastName: d.string, | |
products: d.array(ProductEncoder) | |
}) | |
const UserEncoderGuard = d.guard( | |
d.map(UserEncoder, objectKeysToSnakeCase) // 🦄 after everything is encoded, we can do further transformations | |
) | |
// then just use it | |
UserEncoderGuard({ your_user_object }) |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// types.d.ts | |
/** | |
* This is the default data structure for an appointment that is used in the entire application | |
* An Appointment can be postponed with a reason | |
* If we have a scheduled appointment, it will have a "to" and "from" dates | |
*/ | |
export type Appointment = { | |
postponeReason: string | null | |
latestAppointment?: { | |
to: Date | |
from: Date | |
} | |
} | |
/** | |
* The API accepts a post request, but the payload vary depending on the Appointment | |
* If we are postponing, we must ONLY send "postpone_reason" | |
* If we are scheduling an appointment, then we must send a specific data structure | |
* If we send all data to the API, we receive an error (because some times, things are like that, right? 😂) | |
*/ | |
export type AppointmentEncoded = | |
{ postpone_reason: string } | |
| { appointment: { from: Date } } | |
/** | |
* let's say our form have a business rule: | |
* "isPossible" is a flag, we have a boolean field in the form | |
* is the field is "true" then we must send only the "appointment" data in the request | |
* if the field is "false" then we must sent the "postponeReason" | |
*/ | |
export type AppointmentFormData = { | |
isPossible: boolean | |
postponeReason: string | |
appointment: { | |
from?: Date | |
} | |
} | |
// codables/appointment.ts | |
import * as d from "decoders" | |
import { objectKeysToSnakeCase, objectKeysToCamelCase } from "./utils" | |
const appointmentDecoder: d.Decoder<Appointment> = | |
d.map( | |
d.object({ | |
postpone_reason: d.nullable(d.string), | |
latest_appointment: d.maybe(d.object({ | |
from: d.maybe(d.iso8601), | |
to: d.maybe(d.iso8601), | |
})), | |
}), | |
objectKeysToCamelCase | |
) | |
const appointmentEncoder: d.Decoder<AppointmentEncoded> = | |
d.map( | |
d.map( | |
d.object({ | |
isPossible: d.boolean, | |
postponeReason: d.nullable(d.string), | |
appointment: d.object({ | |
from: d.maybe(d.date) | |
}), | |
}), | |
(({ | |
isPossible, | |
postponeReason, | |
appointment, | |
}) => isPossible ? { appointment } : { postponeReason }) | |
), | |
objectKeysToSnakeCase | |
) | |
export default { | |
decode: d.guard(appointmentDecoder), | |
encode: (data: AppointmentFormData) => d.guard(appointmentEncoder)(data), // just to make sure "data" is actually comming from our form | |
} | |
// api-service.ts | |
import appointmentCodable from "codables/appointment" | |
export const fetchAppointment = (userId) => axios.get(`url-to-api/${userId}/appointment`).then(appointmentCodable.decode) | |
export const saveAppointment = (userId, data: AppointmentFormData) => axios.post(`url-to-api/${userId}/appointment`, appointmentCodable.encode(data)).then(appointmentCodable.decode) |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
const rawData = JSON.stringify({ | |
price: "10", | |
vat: 1.5 | |
}) | |
const api = () => Promise.resolve(JSON.parse(rawData)) | |
const calculateTotalPrice = (product) => product.price + product.vat | |
function init() { | |
api().then((result) => { | |
console.log(`Total price is ${calculateTotalPrice(result)}`) // 11.5 | |
}) | |
} | |
init() |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// index.d.ts | |
/** | |
* This is the default data structure for an appointment that is used in the entire application | |
* An Appointment can be postponed with a reason | |
* If we have a scheduled appointment, it will have a "to" and "from" dates | |
*/ | |
export type Appointment = { | |
postponeReason: string | null | |
latestAppointment?: { | |
to: Date | |
from: Date | |
} | |
} | |
/** | |
* The API accepts a post request, but the payload vary depending on the Appointment | |
* If we are postponing, we must ONLY send "postpone_reason" | |
* If we are scheduling an appointment, then we must send a specific data structure | |
* If we send all data to the API, we receive an error (because some times, things are like that, right? 😂) | |
*/ | |
export type AppointmentEncoded = | |
{ postpone_reason: string } | |
| { appointment: { from: Date } } | |
/** | |
* let's say our form have a business rule: | |
* "isPossible" is a flag, we have a boolean field in the form | |
* is the field is "true" then we must send only the "appointment" data in the request | |
* if the field is "false" then we must sent the "postponeReason" | |
*/ | |
export type AppointmentFormData = { | |
isPossible: boolean | |
postponeReason: string | |
appointment: { | |
from?: Date | |
} | |
} | |
// codables/appointment.js | |
import * as d from "decoders"; | |
import { objectKeysToSnakeCase, objectKeysToCamelCase } from "./utils"; | |
/** | |
* We can use @typedef to define types at application or file level | |
* Bellow we are importing the types from our declaration file and attributing the type to a variable | |
* The annotation is @typedef {your-type-here} NameOfTheVariable | |
* | |
* @typedef {import("./index").Appointment} Appointment | |
* @typedef {import("./index").AppointmentEncoded} AppointmentEncoded | |
* @typedef {import("./index").AppointmentFormData} AppointmentFormData | |
* | |
* Now, we can use directly "Appointment", "AppointmentEncoded", "AppointmentFormData" without having to import every time | |
*/ | |
// You can use types from other libraries normally | |
// Remember that the @type annotation is similar to @typedef | |
// @type {your-type-here} | |
/** @type {d.Decoder<Appointment>} */ | |
const appointmentDecoder = d.map( | |
d.object({ | |
postpone_reason: d.nullable(d.string), | |
latest_appointment: d.maybe( | |
d.object({ | |
from: d.maybe(d.iso8601), | |
to: d.maybe(d.iso8601), | |
}) | |
), | |
}), | |
objectKeysToCamelCase | |
); | |
/** @type {d.Decoder<AppointmentEncoded>} */ | |
const appointmentEncoder = d.map( | |
d.map( | |
d.object({ | |
isPossible: d.boolean, | |
postponeReason: d.nullable(d.string), | |
appointment: d.object({ | |
from: d.maybe(d.date), | |
}), | |
}), | |
({ isPossible, postponeReason, appointment }) => | |
isPossible ? { appointment } : { postponeReason } | |
), | |
objectKeysToSnakeCase | |
); | |
// You are free to type in any way your files, bellow, I could have created a type for the entire object | |
// Or I can provide a type per property | |
export default { | |
decode: d.guard(appointmentDecoder), | |
/** | |
* I can choose to use this annotation to document/type the function params and return | |
* Bellow you will see a different way to document | |
* | |
* @param {AppointmentFormData} data | |
* @returns {AppointmentEncoded} | |
*/ | |
encode: (data) => d.guard(appointmentEncoder)(data), // just to make sure "data" is actually comming from our form | |
}; | |
// api-service.ts | |
import appointmentCodable from "codables/appointment"; | |
export const fetchAppointment = (userId) => | |
axios.get(`url-to-api/${userId}/appointment`).then(appointmentCodable.decode); | |
// Here you can also provide the entire function type, the way I did bellow is equivalent from the "encode" documentation above | |
// You could also extract the function annotation into a declaration file if you prefer | |
/** @type {(userId: number, data: AppointmentFormData) => Promise<Appointment>} */ | |
export const saveAppointment = (userId, data) => | |
axios | |
.post(`url-to-api/${userId}/appointment`, appointmentCodable.encode(data)) | |
.then(appointmentCodable.decode); |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import Vue from "vue" | |
import { fireEvent, render, screen, waitFor } from "@testing-library/vue"; | |
import userEvent from "@testing-library/user-event"; | |
import Element from "element-ui"; | |
import { storeConfig } from "@/store"; | |
import UserView from "@/views/UserView.vue"; | |
import mockServer from "../mockServer"; | |
import githubUserDecoder from "@/codables/githubUserDecoder"; | |
describe("UserView", () => { | |
const build = () => { | |
const view = render(UserView, { store: storeConfig }, (vue) => { | |
vue.use(Element); | |
}); | |
return { | |
view, | |
}; | |
}; | |
test("a user can search for Github usernames", async () => { | |
const server = mockServer(); | |
const octocat = githubUserDecoder(server.schema.first("user")?.attrs); | |
build(); | |
userEvent.type( | |
screen.getByPlaceholderText("Pesquise o usuário"), | |
String(octocat.login) | |
); | |
await Vue.nextTick(); | |
userEvent.click(screen.getByRole("button")); | |
await waitFor(() => expect(screen.getByText(String(octocat.name)))); | |
expect(screen.getByAltText(String(octocat.name))).toHaveAttribute( | |
"src", | |
octocat.avatarUrl | |
); | |
expect(screen.getByText(String(octocat.bio))).toBeInTheDocument(); | |
}); | |
}); |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
type Product = { | |
price: number | |
vat: number | |
} | |
const rawData: string = JSON.stringify({ | |
price: "10", | |
vat: "1,5" | |
}) | |
const api = (): Promise<Product> => Promise.resolve(JSON.parse(rawData)) | |
const calculateTotalPrice = (product: Product): number => product.price + product.vat | |
function init() { | |
api().then((result) => { | |
console.log(`Total price is ${calculateTotalPrice(result)}`) | |
}) | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/** | |
* @typedef {import("./index.d").Product} Product | |
* @typedef {import("./index.d").AppointmentFormData} AppointmentFormData | |
* @typedef {import("./index.d").Appointment} Appointment | |
*/ | |
// PROPS | |
/** | |
* Make use of the `PropOptions` to type your props properly | |
* | |
* @type {import('vue').PropOptions<YOUR_TYPE>} | |
*/ | |
props: { | |
/** @type {import('vue').PropOptions<Product>} */ | |
product: { | |
... | |
} | |
}, | |
// COMPUTED PROPERTIES | |
/** | |
* They are just functions, you can explicitly apply the return value | |
* | |
* @return {YOUR_TYPE} | |
*/ | |
computed: { | |
/** @return {number} */ | |
totalPrice() { | |
return this.product.price + this.product.vat; | |
} | |
} | |
// METHODS | |
/** | |
* Same thing, but you can specificy parameters | |
* | |
* @param {YOUR_TYPE} paramName - param description | |
* @return {YOUR_TYPE} | |
*/ | |
methods: { | |
/** | |
* @param {AppointmentFormData} formData | |
* @return Promise<Appointment> | |
*/ | |
onSubmit(formData) { | |
return this.product.price + this.product.vat; | |
} | |
} | |
// DATA | |
/** | |
* Since data is also a function, you can choose to create a entire type for its return values | |
* or you can even type each property individually | |
* Remember that we have type inference, so a few items don't actually need to be typed | |
*/ | |
data: () => ({ | |
/** @type {boolean} - this does not need to be typed, but you can do it anyway */ | |
isElementVisible: false, | |
/** @type {AppointmentFormData} */ | |
form: { | |
// provide all AppointmentFormData fields, otherwise you will get compile time errors | |
} | |
}) | |
// STORE | |
/** | |
* Vuex does not have a good support for types in Vue 2 | |
* But we can still provide a few things like the state | |
*/ | |
// index.d.ts | |
export type State = { | |
[K in keyof Appointment]?: Appointment[K]; | |
}; | |
// appointment-module.js | |
export default { | |
namespaced: true, | |
/** @type {import("./index.d").State} */ | |
state: { | |
postponeReason: undefined, | |
latestAppointment: undefined | |
}, | |
actions: { | |
/** | |
* @param {import('vuex').ActionContext<State, State>} context | |
* @param {import('./index.d').Appointment} appointment | |
* @returns | |
*/ | |
myAction(context, appointment) { | |
// do something | |
}, | |
} | |
}; | |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment