Skip to content

Instantly share code, notes, and snippets.

@foolmoron
Last active November 17, 2022 18:36
Show Gist options
  • Save foolmoron/3535e4630d8d9e83e64d7579e8faa48d to your computer and use it in GitHub Desktop.
Save foolmoron/3535e4630d8d9e83e64d7579e8faa48d to your computer and use it in GitHub Desktop.
Advanced Typescript Types companion code
// 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