Last active
August 26, 2022 14:27
-
-
Save apieceofbart/ba7600be4be32002310d721b615411ac to your computer and use it in GitHub Desktop.
Typescript advanced examples and common gotchas
This file contains 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
// 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