Skip to content

Instantly share code, notes, and snippets.

@Dobby89
Last active January 17, 2024 08:56
Show Gist options
  • Save Dobby89/d5b8cf7e914904128c7071ad25177057 to your computer and use it in GitHub Desktop.
Save Dobby89/d5b8cf7e914904128c7071ad25177057 to your computer and use it in GitHub Desktop.
Typescript snippets

Helpers

Get a unique array

An array of primitives

[...new Set([1, 1, 2, 3])] // [1, 2, 3]

An array of objects

// https://stackoverflow.com/a/56768137/5243574
export function getUniqueByKey<T>(arr: T[], key: keyof T): T[] {
	return [...new Map(arr.map((item) => [item[key], item])).values()];
}

const objArry = [{ id: 1 }, { id: 1 }, { id: 2 }, { id: 3 }];
getUniqueByKey(objArry, 'id'); // [ { id: 1 }, { id: 2 }, { id: 3 } ]

Format a UTC date to the user's local date

import { isValid } from 'date-fns';

export function getLocalDate(date: Date | string): Date {
	const theDate =
		typeof date === 'string' || typeof date === 'number'
			? new Date(date)
			: date;

	if (!isValid(theDate)) {
		throw new Error('Date is invalid');
	}

	return new Date(theDate.getTime() + theDate.getTimezoneOffset() * 60000);
}

Working with Promise.allSettled

You can filter only the fulfilled promises from a Promise.allSettled call, which also infers the types of the fulfilled promises

type ExtractFulfilled<T extends PromiseSettledResult<any>[]> = Extract<
	T[number],
	PromiseFulfilledResult<any>
>['value'];

function filterFulfilled<T extends Array<PromiseSettledResult<any>>>(
	settled: T
): Array<ExtractFulfilled<T>> {
	const fulfilled = settled.filter(
		(result): result is PromiseFulfilledResult<any> =>
			'status' in result && result.status === 'fulfilled'
	);

	const fulfilledValues = fulfilled.map((result) => result.value);

	return fulfilledValues;
}

const settled = await Promise.allSettled([
    Promise.resolve('foo'),
    Promise.resolve(123),
    Promise.reject(new Error('nope'))
]);

const fulfilledPromises = filterFulfilled(settled);

// You could use something like zod to find/filter the correct item types from the array of fulfilled promises
const foo = fulfilledPromises.find(
    (foo): foo is string => typeof foo === 'string'
);

promiseAllSettledByKey

Inspired by https://stackoverflow.com/a/74098525/5243574

type PlainObj = Record<string, unknown>;

type PromisesMap<T extends PlainObj> = {
	[P in keyof T]: Promise<T[P]> | T[P];
};

type PromiseSettledByKeyResult<T> = {
	[P in keyof T]: PromiseSettledResult<T[P]>;
};

async function promiseAllSettledByKey<T extends PlainObj>(
	promisesMap: PromisesMap<T>
): Promise<PromiseSettledByKeyResult<T>> {
	const settled = await Promise.allSettled(Object.values(promisesMap));

	const toReturn = {} as PromiseSettledByKeyResult<T>;

	Object.keys(promisesMap).forEach(async (key, index) => {
		const result = settled[index];

		if (result.status === 'fulfilled') {
			toReturn[key as keyof T] = {
				status: 'fulfilled',
				value: result.value,
			};
		} else {
			toReturn[key as keyof T] = {
				status: 'rejected',
				reason: result.reason,
			};
		}
	});

	return toReturn;
}

TypeScript Snippets

Diagnose any import issue

./node_modules/typescript/bin/tsc --traceResolution

Prettify

Useful for debugging complex types which extends other types.

Taken from https://www.totaltypescript.com/concepts/the-prettify-helper and https://www.youtube.com/shorts/2lCCKiWGlC0 and https://twitter.com/mattpocockuk/status/1622730173446557697

type Prettify<T> = {
  [K in keyof T]: T[K];
} & {};

const foo: Prettify<ComplexType> = {
  // ...
}

Extract nested types from an existing interface when they may be undefined

Yoinked from https://stackoverflow.com/a/53137082/5243574

interface DeepInterface {
	parentProp?: {
		childProp?: {
			foo: string;
			bar: string;
		};
	};
}

type ParentProp = Exclude<
	DeepInterface['parentProp'],
	null | undefined | never
>;
type ChildProp = Exclude<ParentProp['childProp'], null | undefined | never>;

/*
type ChildProp = {
    foo: string;
    bar: string;
}
*/

Inference

Infer the value of an unamed type using infer and conditional types.

Useful for when a type isn't exported by a library/component. But be aware, this will start to slow the compiler and linter down if used excessively.

type ConstructorArgs<C> = C extends {
	// "infer A" allows us to alias the type of the unknown constructor arg
	new (arg: infer A, ...args: any[]): any; // Find the "newable" function, i.e. constructor
}
	? A
	: never;

class ClassWithUnknownConstructorArgs {
	constructor(arg: { foo?: string; bar?: number }) {
		// ...
	}
}

// Now we can use the ConstructorArgs utility type to find out what 
const constructorArgs: ConstructorArgs<typeof ClassWithUnknownConstructorArgs> = {
	foo: '',
	notAPropertyOnConstructorArgsType: false // This line will error because the type is now known
};

const instance = new ClassWithUnknownConstructorArgs(constructorArgs);

Typeguard

Make sure condition returns the asserted type. The operative thing here is the the return type : valueToTest is Car which effectively casts the type of the variable when used inside the condition.

type Car = {
	make: string;
	model: string;
	year: number;
};

let possibleCar: unknown;

// The boolean returned here is interpreted by TypeScript to respect the claim made by the result of the condition
function isCar(valueToTest: any): valueToTest is Car {
	return (
		valueToTest &&
		typeof valueToTest === 'object' &&
		'make' in valueToTest &&
		typeof valueToTest.make === 'string' &&
		'model' in valueToTest &&
		typeof valueToTest.model === 'string' &&
		'year' in valueToTest &&
		typeof valueToTest.year === 'number'
	);
}

if (isCar(possibleCar)) {
	// `possibleCar` will now be considered to be a type of Car when referenced inside this condition
	possibleCar.make
	
	// Casting to a Car is no longer necessary
	(possibleCar as Car).make
}

Another instance where typeguards can be used is within a filter to assert the types of the array items. Example taken from here

interface MyItem { name: string }

const myItems: (MyItem | undefined)[] = [
  { name: 'c' },
  { name: 'b' },
  undefined,
  { name: 'a' },
]

// a and b will always be "possibly 'undefined'"
myItems.sort((a, b) => (a.name > b.name ? 1 : -1));
// (parameter) a: MyItem | undefined
// Object is possibly 'undefined'

// Chaining a filter before the sort ensures a and b will always be a type of MyItem
myItems
  .filter((item): item is MyItem => !!item)
  .sort((a, b) => (a.name > b.name  ? 1 : -1));

Template Literal Types

Combine types together with template literal string interpolation. Can also transform strings using Uppercase, Lowercase, Capitalize and Uncapitalize (Intrinsic String Manipulation Types).

type Cars = "ford" | "Toyota"
type Colours = "Blue" | "red" | "silver"

type CarColourCombinations = `${Cars}_${Colours}`;
// "ford_Blue" | "ford_red" | "ford_silver" | "Toyota_Blue" | "Toyota_red" | "Toyota_silver"

type CarColourCombinationsUppercase = `${Cars}_${Uppercase<Colours>}`;
// "ford_BLUE" | "ford_RED" | "ford_SILVER" | "toyota_BLUE" | "toyota_RED" | "toyota_SILVER"

type CarColourCombinationsLowercase = `${Cars}_${Lowercase<Colours>}`;
// "ford_Blue" | "ford_red" | "ford_silver" | "Toyota_Blue" | "Toyota_red" | "Toyota_silver"

type CarColourCombinationsCamel = `${Cars}_${Capitalize<Colours>}`;
// "ford_Blue" | "ford_Red" | "ford_Silver" | "Toyota_Blue" | "Toyota_Red" | "Toyota_Silver"

type CarColourCombinationsNormal = `${Cars}_${Uncapitalize<Colours>}`;
// "ford_red" | "ford_silver" | "ford_blue" | "Toyota_red" | "Toyota_silver" | "Toyota_blue"

Filtering properties

type QueryDocKeys = Extract<keyof Document, `query${string}`>
// "queryCommandEnabled" | "queryCommandIndeterm" | "queryCommandState" | "queryCommandSupported" | "queryCommandValue" | "querySelector" | "querySelectorAll"

type QueryDocProps = {
	[Key in QueryDocKeys]: Document[Key]
}
// queryCommandEnabled: (commandId: string) => boolean;
// queryCommandIndeterm: (commandId: string) => boolean;
// queryCommandState: (commandId: string) => boolean;
// queryCommandSupported: (commandId: string) => boolean;
// queryCommandValue: (commandId: string) => string;
// querySelector: {
// 	...;
// };
// ...

Mapped Types

Pick types from an interface which match a value signature, e.g. a method which returns a particular type.

This example will return all method types from the Document interface, which return a HTMLElement or HTMLElement[].

type FilterKeys<T, U> = {[P in keyof T]: U extends T[P] ? P : never}[keyof T] & keyof T;

type OnlyDomMethodKeys = FilterKeys<Document, (...args: any[]) => HTMLElement | HTMLElement[]>
// "onfullscreenchange" | "onfullscreenerror" | "onpointerlockchange" | ... etc

type DomMethods = Pick<Document, OnlyDomMethodKeys>
// {
//     onfullscreenchange: ((this: Document, ev: Event) => any) | null;
//     onfullscreenerror: ((this: Document, ev: Event) => any) | null;
//     onpointerlockchange: ((this: Document, ev: Event) => any) | null;
//     ... 106 more ...;
//     replaceChildren: (...nodes: (string | Node)[]) => void;
// }

Extract values from template literal types

Extract dynamic segments from a URL path.

Example taken from https://stackoverflow.com/a/72331909/5243574.

type _UnwrapParam<P extends string, S extends string[]> = P extends `{${infer Q}}` ? [Q, ...S] : S;

type _Match<
  T extends string,
  S extends string[]
> = T extends `${infer P}/${infer R}`
  ? _Match<R, _UnwrapParam<P, S>>
  : _UnwrapParam<T, S>;

export type Match<T extends string> = _Match<T, []>[number];

export type Extractor<T extends string> = {
  [K in Match<T>]: string;
};

type DynamicSegments = Extractor<'user/{userId}/photo/{photoId}'>
// {
//   userId: string;
//   photoId: string;
// }

Loose Auto Complete

Example taken from https://www.totaltypescript.com/tips/create-autocomplete-helper-which-allows-for-arbitrary-values

type LooseAutoComplete<T extends string> = T | Omit<string, T>;

type CountryCode = LooseAutoComplete<keyof typeof countryCodes>;

const countryCodes = {
  AF: "Afghanistan",
  AL: "Albania",
  DZ: "Algeria",
};

function getCountry(countryCode: CountryCode): string {
  return (
    countryCodes[countryCode as keyof typeof countryCodes] || countryCode.toString()
  );
}

getCountry('AF') // Afghanistan
getCountry('WOOO') // WOOO

Mapped Types

type Animals = keyof typeof animalFoodMap;

export type AnimalFood = {
  [Animal in Animals]: Animal extends Animals
    ? `${Capitalize<Animal>}s like to eat ${(typeof animalFoodMap)[Animal]}`
    : never;
}[Animals];

export const animalFoodMap = {
  elephant: "Acacia leaves and grasses",
  koala: "Eucalyptus leaves",
  orca: "Salmon",
} as const;

export function getAnimalFood<T extends Animals>(
  animal: T
): (typeof animalFoodMap)[T] {
  return animalFoodMap[animal];
}

export const elephantFood: AnimalFood = `Elephants like to eat ${getAnimalFood(
  "elephant"
)}`;
export const koalaFood: AnimalFood = `Koalas like to eat ${getAnimalFood(
  "koala"
)}`;
export const orcaFood: AnimalFood = `Orcas like to eat ${getAnimalFood(
  "orca"
)}`;

Rename Record/Object properties

type PrefixPropsV1<T, Prefix extends string> = {
	[Key in (string & keyof T) as `${Prefix}${Key}`]: T[Key]
};

type PrefixPropsV2<T, Prefix extends string> = {
	[Key in keyof T as Key extends `${infer KeyStr }` ? `${Prefix}${KeyStr}` : Key]: T[Key]
}

const myObj = {
  name: 'Joe',
  age: 30
} as const

type Prefixed1 = PrefixPropsV1<typeof myObj, '_'>
//    ^? type Prefixed = { _age: 30; _name: "Joe"; }

type Prefixed2 = PrefixPropsV2<typeof myObj, '_'>
//    ^? type Prefixed = { _age: 30; _name: "Joe"; }

Remove Record/Object properties By Prefix

type RemovePropsWithPrefix<T, Prefix extends string> = Omit<T, `${Prefix}${string}`>

const person = {
  secretName: 'Superman',
  name: 'Clark Kent'
} as const

type RemovedSecrets = RemovePropsWithPrefix<typeof person, 'secret'>
//    ^? type RemovedSecrets = { name: "Clark Kent"; }

Reverse String

type Reverse<T extends string> = T extends `${infer FirstChar}${infer RemainingChars}`
	? `${Reverse<RemainingChars>}${FirstChar}`
	: "";
	
type Reversed = Reverse<'Hello'>
//    ^? type Reversed = "olleH"
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment