Skip to content

Instantly share code, notes, and snippets.

@martensonbj
Last active August 27, 2021 18:35
Show Gist options
  • Save martensonbj/69446fa0b75cc31d28a538b38e464025 to your computer and use it in GitHub Desktop.
Save martensonbj/69446fa0b75cc31d28a538b38e464025 to your computer and use it in GitHub Desktop.
# Typescript Overview
## Why TS?
JavaScript is notoriously lenient in its loosely typed structure which
can cause unexpected bugs that are difficult to track down. TypeScript solves
this problem by throwing an error at compile time when defined types don't match
what is being passed around.
This also eliminates the need for boilerplate tests that simply verify that a function/component receives a prop or argument of the expected type and returns the expected type of value (or lack thereof).
Consider the following contrived example:
```js
const doSomeMath = (num1, num2) => num1 + num2
```
The above code will allow you to pass whatever makes you happy as the arguments
`num1` and `num2`, even though this function (and whatever you're doing with its
return value) might consistently need to be a number.
Typescript allows you to define what type an argument should be, avoiding
unforseen pitfalls that come from unknowingly passing around unexpected types.
```ts
const doSomeMath = (num1: number, num2: number): number => num1 + num2
```
## Installation
At the time of writing we are using `"typescript": "3.7.5"`, as noted in `devDependencies` found within `package.json`.
This release allows us to use newer syntax like [Optional
Chaining](https://www.typescriptlang.org/docs/handbook/release-notes/typescript-3-7.html#optional-chaining)
and [Nullish Coalescing](https://www.typescriptlang.org/docs/handbook/release-notes/typescript-3-7.html#nullish-coalescing) to handle potentially undefined properties on objects.
Combined, we can concisely check for null or undefined values, and figure out what to use as a default so our components render consistently and Typescript is happy.
### Optional Chaining
```ts
// Before
if (foo && foo.bar && foo.bar.baz) {
// ...
}
// After-ish
if (foo?.bar?.baz) {
// ...
}
```
### Nullish Coalescing:
```ts
// If foo exists use foo, if not use bar()
let x = foo ?? bar()
```
### Combining the two:
```ts
const MyComponent = (applicant: ApplicantType) => {
const firstName = applicant?.firstName ?? 'Unknown First Name'
const jobs = applicant?.jobs ?? []
// Continue rendering the rest of the component with safe prop values
}
```
## Where To Find Stuff // TODO *********************
There are a few important TypeScript files that are generating the types and handling some TS things for what currently lives in the `client` directory (aka `customer`).
Most of the heavy lifting is done in `platform/client/src/graphQL`
#### Generated Types
`/graphQL/types.ts`
After running through all of the `graphQL` query strings and then merging that information with anything manually defined within the resolvers (discussed next), this file get automatically generated with all of the type definitions.
## Defining Types
### Custom Type
```ts
type Candidate {
id: string
name?: string
currentJob: string
jobApplications: []Application
}
```
- The `?` within a type definition indicates that the prop `name` on the object is optional and might be undefined.
- Note that fields on a custom type can either be a primitive (like `string`), or another custom type (like `Application` which is defined elswhere, like `Candidate`)
- The syntax `[]Application` indicates an array of elements of type `Application` (more on this later)
### Union Type
A set of type literals that define the options for what type a field *could* be
```ts
type Candidate {
id: string | number
}
```
Useful for if something could be null or undefined
```ts
type Candidate {
id: string | null | undefined
}
```
### Enum
An enum assigns a series of constants to numerical values that allow for more consistent value definitions across an app.
Behind the scenes, the numerical values start at 0 and do not need to be defined.
```ts
enum InterestLevel {
INTERESTED,
NOT_INTERESTED,
INDETERMINATE
}
```
Behind the scenes this enum looks like:
```ts
enum InterestLevel {
INTERESTED = 0,
NOT_INTERESTED = 1,
INDETERMINATE = 2
}
```
##### Additional Notes (not often used in our app):
You can set a different default starting value:
```ts
enum Pilots {
MAVERICK = 1,
GOOSE,
ICEMAN
}
```
You can also assign specific values to each element:
```ts
enum Pilots {
MAVERICK = 1,
GOOSE = 3,
ICEMAN = 5
}
```
Values are read similar to using object notation:
`InterestLevel.INTERESTED // ==> 0`
You can also read an enum through it's numerical value similar to reading an element within an array. This will give you back the string value of the enum.
`InterestLevel[0] // ==> INTERESTED`
## Using Typescript
### 1. Typing A Variable
The type literal is set with a colon after the variable name, before assigning it to a value.
```ts
let name: string = "Maverick"
name = "Goose" // OK
name = 100 // FAIL
```
### 2. Typing an Argument
```ts
const someFunc = (arg1: string, arg2: number) => { console.log(name) }
// someFunc('Maverick', 100) // OK
// someFunc(100, 'Maverick') // FAIL
```
### 3. Typing a Destructured Parameter
The JS function before adding types:
```js
const handleCandidate = ({ candidate }) => {
// do stuff
}
```
Defining the types within the function signature:
```ts
const handleCandidate = ({ candidate }: { id: string, name: string }) => {
// do stuff
}
```
Defining the type externally:
- This is the most common pattern within our app for consistency and readability
- Particularly important when the incoming parameter has a lot of fields to avoid an insanely long function signature
```ts
type CandidateType {
id: string
name: string
// imagine there are 100 more fields here
}
const handleCandidate = ({ candidate }: CandidateType) => {
// do stuff
}
```
### Typing a Function's Return Value
To type a return value, the type lives between the parenthases defining incoming parameters and the fat arrow, separated by a comma.
```ts
const printDate = (): number => {
return Date.now()
}
printDate() ==> 1581689503172 // OK
const printDate = (): number => {
return Date.now().toString()
}
printDate() ===> "1581689503172" // ERROR
```
## Type Literal Cheat Sheet
Each of the below definitions contains:
1. The typename or pattern used to indicate the type
2. An example of a value that would satisfy that type
3. An example in code
Note that in most cases, TypeScript can infer the type of a variable without requiring explicit definition.
In the examples below the types aren't necessary, but are included for demonstration purposes.
This list can also be found [in the docs](https://www.typescriptlang.org/docs/handbook/basic-types.html)
##### String:
- typename: `string`
- example: `'hello world'`
- in code:
```ts
let name: string = 'Elvis'
```
##### Number:
- typename: `number`
- example: `8` (note: all are considered floats)
- in code:
```ts
let age: number = 21
```
##### Boolean:
- typename: `boolean`
- example: `false`
- in code:
```ts
let isAdmin: boolean = true
```
##### Array:
- typename:
- `number[]` (typename followed by `[]`)
- `Array<number>` (`Array` followed by the typename wrapped in `<>`)
- example: `[1, 2, 3]`
- in code:
```ts
type Candidate {
id: number
name: string
}
let candidates: []Candidate = [candidate1, candidate2, candidate3]
let candidates: Array<Candidate> = [candidate1, candidate2, candidate3]
```
- *note: TS specifically requires types when an array is, or could be, empty since it can't infer the type of it's potential elements*
##### Tuple:
- typename: `[string, number]` (an array with a specific and immutable series of types both in order and length)
- example: `['hello', 1]`
- in code:
```
let items: [string, number] = ['yo', 10] // ==> OK
let items: [string, number] = [10, 'yo'] // ==> ERROR
let items: [string, number] = ['yo', 10, 'something'] // ==> ERROR
```
##### Enum:
- typename: `InterestLevel` (name of defined enum type)
- example: `InterestLevel.INTERESTED`
- in code:
```
enum InterestLevel {
INTERESTED,
NOT_INTERESTED,
INDETERMINATE
}
let interest: InterestLevel = InterestLevel.INTERESTED // OK
let interest: InterestLevel = 'CONTACTED' // ERROR (not part of enum)
```
##### Object:
- typename: `object`
- example: `{age: 16}`
- in code:
```ts
const someFunc = (o: object) => {// do stuff}
someFunc({age: 16}) // OK
someFunc(16) // ERROR
```
##### Any:
- typename: `any` (a catch-all for when you do not know a type literal, or are expecting an unknown combination of types)
- example: could be `1` or `'1'` or `[1, 'hello', Candidate]` or `Candidate[]` or `undefined` (admittedly an unlikely series of possible types but for argument sake...)
- in code:
```
let items: any = mysteryOrUnpredictableItem
```
##### Functions:
- typename: `() => {}` (the signature of the expected function)
- What the function is expecting as parameters (here nothing) `()`
- What the function should return (here also nothing) `{}`
- example: `() => { console.log(arg1) }`
- in code:
```
const printThings = () => console.log('hey!')
const returnStuff = () => 2 + 2
const getAndReturnStuff = (arg1: string) => arg1.split()
let func1: () => {} = printThings() // ==> OK
let func2: () => number = returnStuff() // ==> OK
let func2: () => {} = returnStuff() // ==> ERROR
let func3: (string) => string[] = getAndReturnStuff() // ==> OK
let func3: () => {} = getAndReturnStuff() // ==> ERROR
```
### "Empty" Types
#### Void
- typename: `void`
- The abscence of any type at all (the opposite of `any`)
- Most commonly used to indicate a function doesn't return anything (either explicitly or implicitly)
- example: (nothingness in every way 🤷‍♀️)
- in code:
```ts
type PropsType {
id: string
handleClick: () => void
}
const someFunc = () => { console.log('sup')} // ==> OK
const someFunc = () => undefined // ==> OK
const someFunc = () => { } // ==> OK
const someFunc = () => Date.now() // ==> ERROR
```
##### Null
- typename: `null` (different from both `void` and `undefined`)
- An intentionally empty value, indicates that a variable points to no relevant object (*yet*, its probably expected to have a value at some point) [read more here](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/null)
- Commonly set as the default value from an API response if a value doesn't exist yet (as opposed to an empty string, for example)
- Used to account for a variety of possibly empty values of a thing
- example: (nothingness indicated with the intentional empty global primitive value `null`)
- in code:
```ts
let candidates: []Candidates | null = resultsFromAPI
```
##### Undefined
- typename: `undefined`
- An empty value that represents the global variable `undefined` [read more on here](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/undefined)
- Represents a variable that has not been assigned a value
- example: (nothingness indicated by the global primitive variable `undefined`)
- in code:
```ts
let candidateObjFromAPI
let candidates: Candidate | undefined = candidateObjFromAPI // ==> OK
let candidates: Candidate | null = candidateObjFromAPI // ERROR (null !== undefined)
```
##### Never
- typename: `never`
- Indicates a situation where a function will ALWAYS throw an error, or never return a value.
- Indicates a variable that has too many type guards and can never have a successful value
- example: An infinite while loop
- in code:
```ts
const dudeWheresMyCar = () => {
while (true) {
console.log('And thhhennnnn??')
console.log('NO AND THEN!!')
}
}
```
## Type Assertion
Occasionally you will need to be extra explicit about what type something is to assuage TypeScript's very specific needs.
// *********** TODO: Find an example error for when this is needed *****************
There are two ways to forcefully assert/confirm a type.
```ts
type JobApplication {
title: string
city: string
}
type Candidate {
jobApplications: JobApplication[]
}
// Example without TS for comparison:
const jobTitles = (jobApplications).map(jobApp) => {
return jobApp.title
})
// 1. "angle bracket" syntax
const jobTitles = (<JobApplication[]>jobApplications).map((jobApp: JobApplication) => {
return jobApp.title
})
// 2. "as" syntax
const jobTitles = (jobApplications as JobApplication[]).map((jobApp: JobApplication) => {
return jobApp.title
})
```
### Type Assertion & Import Statements
You'll often use the `as` type assertion syntax to shorten lengthy, automatically generated graphql types in import statements.
This makes tossing around that type in your component much more pleasant for you, and much more readable to future you (and everyone else).
Without `as` Syntax:
```ts
import {
getApplicantJobs_applicantJobs_results_Applicant
} from '../graphql/types'
type Job {
applicants: getApplicantJobs_applicantJobs_results_Applicant[]
title: string
}
// ... later on in component logic
const someFunc = (applicant: getApplicantJobs_applicantJobs_results_Applicant) => {
// do stuff
}
```
With `as` Syntax:
```ts
import {
getApplicantJobs_applicantJobs_results_Applicant as Applicant
} from '../graphql/types'
type Job {
applicants: Applicant[]
title: string
}
// ... later on in component logic
const someFunc = (applicant: Applicant) => {
// do stuff
}
```
## Generics
Generics allow you to be more dynamic with what type a function might be working with.
They allow you to say "Whatever the type is, make sure that type is the same type that is used everywhere else within this function."
Often, this "generic" type is indicated with the syntax `<Type>` or `<T>`, although you can call it whatever you want. (Think of the `i` variable when setting up a for loop - you could call that `banana` instead of `i` if you wanted to and then use `banana` everywhere else in the loop, but convention decided to use `i`)
Example:
```ts
function wrapSomethingInAnArray<Type>(input: Type): Type[] {
return [input];
}
```
In the above example, the `<Type>` generic tells TS to translate the word `Type` into whatever type is passed in and then used throughout the function. You can pass in whatever type you want (`string`, `integer`, `Candidate`...) but that is the type that will be used throughout the function anywhere `Type` is referenced, including the return value.
```ts
const stringArray: string[] = wrapInArray('hello') // ==> ['hello'] OK
const numArray: number[] = wrapInArray(123) // ==> [123] OK
const oopsArray: string[] = wrapInArray(123) // ==> ERROR, type passed in must be type in array returned
```
### Generics In Apollo
```ts
import {
getUser as Data,
getUserVariables as Variables,
} from '../graphql/types/schema'
const { data, error, loading } = useQuery<Data, Variables>(
GET_USER_QUERY,
{
variables: { id: 1 },
},
)
```
In the above example, we are using the `useQuery` hook from `@apollo/react-hooks` which is expecting two Generics: `Data` and `Variables`.
These variables are defined in the import statements above the function call, which were generated automatically by the graphql schema from our manually defined graphql queries.
`<Data>` here tells TypeScript to trace the shape of that `Data` type from our import statement all the way through the `useQuery` function to the return value, shown here with the destructured set of variables `{ data, error, loading }`.
Similarly, `<Variables>` tells TS to make sure the arguments passed into the query (here we pass `{id: 1}` must match the parameters expected for that specific query.
It continues to keep an eye on the type of `data` we get returned from the function call, making sure that if you read a property on `data` (like `data.username` as a hypothetical example), `username` is actually a property that the Generic `<Data>` shape originally had available.
You can check out how Apollo writes the actual `useQuery` function in TS leveraging those Generics [here](https://github.com/apollographql/apollo-client/blob/0340c48429b20f621d552af748527798bf6f26c5/src/react/hooks/useQuery.ts)
## Error Pattern Cheat Sheet
// TODO: COLLECT ERRORS
### The "Obvious" ones
```ts
error TS2531: Object is possibly 'null'.
// Translation:
Somewhere you are trying to access a property on an object without setting up a default value, or verifying that the object itself is not null.
// Example:
const isAdmin = data.currentUser.isAdmin
// Solution:
OptChaining/Nullish Coalescing
const isAdmin = data?.currentUser?.isAdmin ?? false
```
```ts
error TS2345: Argument of type 'string[]' is not assignable to parameter of type 'string'.
// Translation:
Somewhere you are specifying that a parameter should be a string, and instead it is receiving an array of strings.
// Example:
const someFunc = (name: string) => {
console.log(name)
}
const names = ['Bob', 'Joe']
someFunc(names) // ERROR
// Solution:
someFunc(names[0]) // OK
```
```ts
error TS7031: Binding element 'applicants' implicitly has an 'any' type.
// Translation:
Somewhere an argument is missing a type assignment, so TS is defaulting the type to 'any' and is cranky about it.
// Example:
const Applicants = ({ applicants }) => {
// Component Logic Here
}
// Solution:
const Applicants = ({ applicants }: { applicants: Applicant[] }) => {
// Component Logic Here
}
```
### The Verbose Ones
```ts
// Example
doSomethingWithJobs(jobs)
// function declaration:
function doSomethingWithJobs(jobs: Job[]) {
// do stuff here
}
// Error
error TS2345: Argument of type 'readonly (getApplicantJobs_applicantJobs_results_Applicant | getApplicantJobs_applicantJobs_results_Job | null)[]' is not assignable to parameter of type '(getApplicantJobs_applicantJobs_results_Job | getJob_job_jobRequisitions)[]'.
The type 'readonly (getApplicantJobs_applicantJobs_results_Applicant | getApplicantJobs_applicantJobs_results_Job | null)[]' is 'readonly' and cannot be assigned to the mutable type '(getApplicantJobs_applicantJobs_results_Job | getJob_job_jobRequisitions)[]'.
// Translation:
// Somewhere, an parameter is expected to be a readonly array of an explicit type, and you are sending through an argument of something vague, mutable, or of a potentially illegal type.
// Translation v2:
// The argument you are passing could be on of the following:
getApplicantJobs_applicantJobs_results_Applicant[]
getApplicantJobs_applicantJobs_results_Job[]
null
// The parameter the function is looking for must be one of the following:
getApplicantJobs_applicantJobs_results_Job[]
getJob_job_jobRequisitions[]
// Since TS doesn't know which you could mean, you need to be EXTRA explicit in the function call with one of the legal type options that TS is aware of (ie: Type Assertion!).
// Solution
doSomethingWithJobs(jobs as Job[])
// or
doSomethingWithJobs(<Job[]>jobs)
// function declaration:
function doSomethingWithJobs(jobs: Job[]) {
// do stuff here
}
```
```ts
// Example
onClick = (e: React.SyntheticEvent) => setState(e.currentTarget.value)
// Error
Error [tsserver 2339] Property 'value' does not exist on type 'EventTarget & Element'.
// Translation
// TS Doesn't know what kind of HTML element this event is firing on, some of which don't have the `target` or `value` properties.
// Solution
// Cast the incoming event argument as the specific HTML element the event is firing on
onClick = (e: React.SyntheticEvent<HTMLInputElement>) => setState(e.currentTarget.value)
```
```ts
// Example
const InputField = styled.input<useCssType>(
useCss,
({ invalidText }) => ({
'&[invalid] + span::after': {
content: `"${invalidText || ''}"`,
color: COLORS.orange.level4,
position: 'absolute',
top: '2rem'
},
}),
)
// Error
[tsserver 2339] [Error] Property 'invalidText' does not exist on type 'Pick<DetailedHTMLProps<Element>....useCssType & ThemeProps<...>
// Translation
// You have a styled component that is defined with the type `useCssType` (or maybe isn't using any defined types), but you are also trying to tell that style object to recognize a specific additional prop, or set of props.
// Solution
// Include the specific prop next to the type cast.
const InputField = styled.input<useCssType & { invalidText?: string }>(
useCss,
({ invalidText }) => ({
'&[invalid] + span::after': {
content: `"${invalidText || ''}"`,
color: COLORS.orange.level4,
position: 'absolute',
top: '2rem'
},
}),
)
```
## Resources
[TypeScript Docs](https://www.typescriptlang.org/index.html)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment