Skip to content

Instantly share code, notes, and snippets.

@apieceofbart
Last active August 26, 2022 14:27
Show Gist options
  • Save apieceofbart/ba7600be4be32002310d721b615411ac to your computer and use it in GitHub Desktop.
Save apieceofbart/ba7600be4be32002310d721b615411ac to your computer and use it in GitHub Desktop.
Typescript advanced examples and common gotchas
// EXAMPLE 1
// Let's say you want to create a function that returns different types based on argument passed. Contrived example:
function returnNumberOrString(returnString: boolean) {
if (returnString) {
return "42" as string // cast to string to avoid literal type
}
return 42 as number // cast to number to avoid literal type
}
// if you run this, infered return type of function will be `string | number` which is not very helpful,
// no matter what is the argument value
const s = returnNumberOrString(true) // s is `string | number`, no autocompletion etc.
// How to fix that? With the function overload!
// adding those two lines above the function definition will make sure TS correctly infers types
function returnNumberOrString(returnString: true): string
function returnNumberOrString(returnString: false): number
function returnNumberOrString(returnString: boolean) {
if (returnString) {
return "42" as string // cast to string to avoid literal type
}
return 42 as number // cast to number to avoid literal type
}
const s = returnNumberOrString(true) // s is now string, yay!
// You can also use combination of generics and conditional types to achieve the same effect:
function returnNumberOrString<T extends boolean>(returnString: T): T extends true ? string : number
function returnNumberOrString(returnString: boolean) {
if (returnString) {
return "42" as string // cast to string to avoid literal type
}
return 42 as number // cast to number to avoid literal type
}
const s = returnNumberOrString(true) // s is again string
// EXAMPLE 2
// How to get a property of an object with typed inferred?
function getProp<TObject,TKey extends keyof TObject>(obj: TObject, prop: TKey) {
return obj[prop]
}
const developer = {
salary: 1_000_000,
name: "John"
}
const developerName = getProp(developer, "salary") // developerName is `string`
// EXAMPLE 3
// Let's say you have a function that gets you some data.
// For example you fetch them using react hook and the hook is generic with the data format as a type
// You can also pass a optional transform function which will transform your data from Input to Output type
// How can we type that avoiding passing Output type - we would like TS to infer that from transform function
// We might try overloading for start:
function request<T>(): T {
return "12" as unknown as T
}
function getData<Input>(transform?: undefined): Input
function getData<Input, Output>(transform: (data: Input) => Output): Output
function getData<Input, Output>(transform?: (data: Input) => Output) {
const data = request<Input>()
if (transform) {
return transform(data)
}
return data
}
let outputAsString = getData<string>()
let outputAsNumber = getData<string, number>(data => data.length) // this works but how to avoid second generic?
// The answer is: using currying:
function getDataCurried<Input>() {
return function <Output>(transform?: (data: Input) => Output) {
const data = request<Input>();
if (transform) {
return transform(data);
}
return data;
} as {
(): Input;
<Output>(transform: (data: Input) => Output): Output;
};
}
const getData = getDataCurried<string>();
outputAsString = getData(); // this is fine
outputAsNumber = getData((data: string) => data.length); // correctly inferred to be number
// EXAMPLE 4
// Exclusive union types
// Let's say you want to have an object with 3 properties, logical operators (or, and, xor)
// but allow only one of them to be used
// You might start with this union type:
type BitwiseCondition =
| { or: number }
| { xor: number }
| { and: number }
// This however will allow for:
const query: BitwiseCondition = {
or: 12,
xor: 42 // we would like this to be error
}
// Typescript doesn't support exclusive union types but you can use this workaround:
interface And {
and: number
or?: undefined
xor?: undefined
}
interface Or {
or: number
and?: undefined
xor?: undefined
}
interface Xor {
xor: number
or?: undefined
and?: undefined
}
export type BitwiseCondition = And | Or | Xor
// this will cause TS to error
const query: BitwiseCondition = {
or: 12,
xor: 42
};
// EXAMPLE 5
// Let's say you want to filter out undefined values from array
const filteredWrong = ['aaa', undefined, 'ccc'].filter(Boolean)
// unfortunately in this case, `filteredWrong` will be infered as (string|undefined)[]
// the way to solve that is to use type guard (`x is T` as a return type in the code below)
function notUndefined<T>(x: T | undefined): x is T {
if (x === undefined) {
return false
}
return true
}
let filteredRight = ['aaa', undefined, 'ccc'].filter(notUndefined) // filteredRight is infered as string[]
// EXAMPLE 6
// TODO
// https://stackoverflow.com/questions/65845621/dealing-with-default-values-with-destructuring-and-discriminated-union
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment