Last active
November 17, 2022 18:36
-
-
Save foolmoron/3535e4630d8d9e83e64d7579e8faa48d to your computer and use it in GitHub Desktop.
Advanced Typescript Types companion code
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
// To follow along using VSCode, copy/paste this code into | |
// a TS file (using TS 4.8.2+), and enable the VSCode | |
// js/ts.implicitProjectConfig.strictNullChecks setting | |
const obj = { | |
nested: { | |
prop1: 1, | |
prop2: 'a', | |
} | |
} | |
// 1C. Readability & Maintainability | |
{ | |
function pick1(target: any, key: string): any { | |
return target[key]; | |
} | |
pick1({ foo: 'bar' }, 'foo'); // string | |
} | |
{ | |
function pick2<T, K extends keyof T>(target: T, key: K): T[K] { | |
return target[key]; | |
} | |
pick2({ foo: 'bar' }, 'foo'); // 'bar' | |
} | |
// 1C. Duck Typing | |
{ | |
let obj1: { nested: { prop1: number, prop2: string } } = obj | |
let obj2: { prop1: number } | |
obj2 = obj1.nested | |
} | |
// 1D. Set Theory | |
{ | |
let str1: string | |
let str2: 'a' | 'b' | 'c' = 'b' | |
str1 = str2 | |
str2 = str1 // Type 'string' is not assignable to type '"a" | "b" | "c"' | |
} | |
// 2A. Inline Types | |
{ | |
let obj1: { nested: { prop1: number, prop2: string } } = obj | |
let obj2: { prop1: number } | |
obj2 = obj1.nested | |
} | |
// 2B. Named Types | |
{ | |
interface Num { | |
prop1: number | |
} | |
interface NumAndString extends Num { | |
prop2: string | |
} | |
interface HasNested { | |
nested: NumAndString | |
} | |
let obj1: HasNested = obj | |
let obj2: NumAndString | |
obj2 = obj1.nested | |
console.log(obj2.prop2) | |
} | |
{ | |
type Num = { | |
prop1: number | |
} | |
type NumAndString = Num & { | |
prop2: string | |
} | |
type HasNested = { | |
nested: NumAndString | |
} | |
let obj1: HasNested = obj | |
let obj2: NumAndString | |
obj2 = obj1.nested | |
console.log(obj2.prop2) | |
} | |
// 2C. Referenced Types | |
{ | |
type HasNested = { | |
nested: { | |
prop1: number | |
prop2: string | |
} | |
} | |
let obj1: HasNested = obj | |
let obj2: HasNested['nested'] | |
obj2 = obj1.nested | |
console.log(obj2.prop2) | |
} | |
// 2D. typeof & keyof | |
{ | |
const myObj = { | |
name: 'SomeName', | |
pets: ['cat', 'turtle'], | |
} | |
type Obj = typeof myObj // { name: string; pets: string[]; } | |
type ObjKeys = keyof Obj // 'name' | 'pets' | |
function deleteObjKey(obj: Obj, key: keyof Obj) { | |
delete obj[key] | |
} | |
} | |
// 2E. Mapped Types | |
{ | |
type MapFromStringToElement = { | |
[key: string]: HTMLElement | null; | |
} | |
const elements: MapFromStringToElement = { | |
body: document.querySelector('body'), | |
img: document.querySelector('img'), | |
audio: document.querySelector('audio'), | |
} | |
} | |
// 2F. Template String Types | |
{ | |
let str: `prefix-${string}-suffix` | |
str = 'prefix-hello-suffix' | |
str = 'prefiz-world-suffix' // Type '"prefiz-world-suffix"' is not assignable to type '`prefix-${string}-suffix`' | |
type Flavor = 'Vanilla' | 'Chocolate' | 'Strawberry' | |
type Topping = 'Sprinkles' | 'Oreos' | 'Peanuts' | |
type IceCream = `${number} scoops of ${Flavor} with ${Topping} and ${Topping}` | |
const myDessert: IceCream = '2 scoops of Vanilla with Sprinkles and Peanuts' | |
} | |
// 2G. Tuple Types | |
{ | |
const tuple: [string, boolean, ...number[]] = ['str', true, 9, -4, 55] | |
let str: string = tuple[0] | |
let bool: boolean = tuple[1] | |
let num1: number = tuple[2] | |
let num2: number = tuple[3] | |
let num3: number = tuple[4] | |
let num4: number = tuple[5] // doesn't warn that it is undefined by default (needs --noUncheckedIndexedAccess) | |
} | |
// 2H. Built-In Utility Types | |
{ | |
function doSomething(arg1: string, arg2: number, arg3: boolean) { return } | |
function doSomethingDEBUG(...params: Parameters<typeof doSomething>) { | |
console.log(params) | |
return doSomething(...params) | |
} | |
} | |
// 3A. const & as const | |
{ | |
const list1 = ['a', 'b', 'c'] // string[] | |
let list2 = ['a', 'b', 'c'] as const // readonly ["a", "b", "c"] | |
} | |
{ | |
const tuple = ['str', true, 9, -4, 55] as const | |
let num4: number = tuple[5] // Tuple type 'readonly ["str", true, 9, -4, 55]' of length '5' has no element at index '5' | |
} | |
// 3B. Asserting with Errors | |
{ | |
const config: { FLAG ?: string } = { FLAG: 'dev' } | |
const flag = config.FLAG // string | undefined | |
if ( | |
config.FLAG == null || | |
config.FLAG != 'dev' && | |
config.FLAG != 'test' && | |
config.FLAG != 'release' | |
) { | |
throw new Error(`Environment variable CONFIG_FLAG must be 'dev' or 'test' or 'release`) | |
} | |
const sameFlag = config.FLAG // "dev" | "test" | "release" | |
} | |
// 3C. Discriminated Union Types | |
{ | |
function getRandomShape(): Shape { | |
return { | |
kind: 'square', | |
size: 5, | |
} | |
} | |
interface Square { | |
kind: 'square'; | |
size: number; | |
} | |
interface Rectangle { | |
kind: 'rectangle'; | |
width: number; | |
height: number; | |
} | |
type Shape = Square | Rectangle; | |
let shape: Shape = getRandomShape() | |
if (shape.kind === 'square') { | |
console.log(shape.size) | |
} else if (shape.kind === 'rectangle') { | |
console.log(shape.width, shape.height) | |
} | |
} | |
// 3D. User-Defined Type Guards | |
{ | |
interface ModernObject { | |
save(): void | |
} | |
interface LegacyObject { | |
serialize(): void | |
} | |
function getObjectFromDatabase() : ModernObject | LegacyObject { | |
return { | |
save() { }, | |
} | |
} | |
function isModern(obj: ModernObject | LegacyObject): obj is ModernObject { | |
return (obj as ModernObject).save !== undefined; | |
} | |
let obj = getObjectFromDatabase(); // ModernObject | LegacyObject | |
if (isModern(obj)) { | |
obj.save(); // ModernObject | |
} else { | |
obj.serialize(); // LegacyObject | |
} | |
} | |
// 3E. Forced Type Assertions | |
{ | |
function pickProps<T extends string>(obj: Record<T, string>, props: T[]) { | |
const pickedObj: Record<string, string> = {} | |
for (const p of props) { | |
pickedObj[p] = obj[p] | |
} | |
// Assert that the new object type only contains | |
// the set of keys provided in the props array | |
return pickedObj as { [key in T]: string } | |
} | |
} | |
// 4A. Custom Generics | |
{ | |
type MyArray<T> = T[] | |
type MakePartialWithString<T> = Partial<T> | string | |
// Higher-order generic composition doesn't work :() | |
// 6+ years feature request https://github.com/microsoft/TypeScript/issues/1213 | |
type MyArrayPartialWithString<T> = MakePartialWithString<MyArray> | |
} | |
{ | |
interface SomeObj { | |
A ?: 1 | |
B ?: 2 | |
C ?: 3 | |
} | |
const map: SomeObj = { | |
A: 1, | |
} | |
function nonGenericFunc(prop: keyof SomeObj): NonNullable<SomeObj[keyof SomeObj]> { | |
const value = map[prop] | |
if (!value) { | |
throw new TypeError(`Cannot find property ${prop}`) | |
} | |
return value | |
} | |
const abc: 1 | 2 | 3 = nonGenericFunc('B') | |
// Generic function body and runtime behavior is the same as | |
// the non-generic, only difference is the type inference | |
function genericFunc<T extends keyof SomeObj>(prop: T): NonNullable<SomeObj[T]> { | |
const value = map[prop] | |
if (!value) { | |
throw new TypeError(`Cannot find property ${prop}`) | |
} | |
return value | |
} | |
const a: 1 = genericFunc('A') | |
const b: 2 = genericFunc('B') | |
const c: 3 = genericFunc('C') | |
} | |
{ | |
type MapFromStringToElement<T extends keyof HTMLElementTagNameMap> = { | |
[key in T]: HTMLElementTagNameMap[key] | null; | |
} | |
const elements: MapFromStringToElement<'body' | 'img' | 'audio'> = { | |
body: document.querySelector('body'), | |
img: document.querySelector('img'), | |
audio: document.querySelector('audio'), | |
} | |
} | |
// 4B. Infer Generics | |
{ | |
type Dictionary<TValue> = Record<string, TValue> | |
type DictionaryValue<TDictionary> = TDictionary extends Dictionary<infer TValue> | |
? TValue | |
: never | |
type BoolDictionary = Dictionary<boolean> | |
type BoolType = DictionaryValue<BoolDictionary> // boolean | |
type SomeObjDictionary = Dictionary<{ a: number, b: string }> | |
type SomeObjType = DictionaryValue<SomeObjDictionary> // { a: number, b: string } | |
} | |
// 4C. Recursive Generics | |
{ | |
type Json = string | number | boolean | null | Json[] | { [key: string]: Json } | |
} | |
{ | |
type Flatten<TItem> = TItem extends readonly [infer TFirst, ...infer TRest] | |
? [...Flatten<TFirst>, ...Flatten<TRest>] | |
: TItem extends [] | |
? TItem | |
: [TItem]; | |
function deepFlatten<T extends readonly unknown[]>(array: T): Flatten<T> { | |
// Code that actually flattens the array | |
let flattened: unknown[] = [] | |
for (const item of array) { | |
if (Array.isArray(item)) { | |
flattened = flattened.concat(deepFlatten(item)) | |
} else { | |
flattened.push(item) | |
} | |
} | |
return flattened as Flatten<T> | |
} | |
const items = deepFlatten([1,['a',2,[[true]],3]] as const) // [1, "a", 2, true, 3] | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment