Typescript Utilities
Here's a collection of some utility types I've put together:
Functions
import { Reverse } from './arrays'
/**
* A function with defined input and output
*/
export type Fn<TParams, TResult> = (params: TParams) => TResult
/**
* A function that returns a result or undefined
*/
export type OptionFn<TParams, TResult> = Fn<TParams, TResult | undefined>
/**
* A function that returns void
*/
export type VoidFn<TParams> = (params: TParams) => void
/**
* Convert a function type into an async version of that function
*/
export type Async<TFn extends (...args: any[]) => any> = (
...args: Parameters<TFn>
) => Promise<ReturnType<TFn>>
/**
* Create an async version of `Fn`
*/
export type AsyncFn<TParams, TResult> = Async<Fn<TParams, TResult>>
/**
* Create an async version of `OptionFn`
*/
export type AsyncOptionFn<TParams, TResult> = Async<OptionFn<TParams, TResult>>
/**
* Create an async version of `VoidFn`
*/
export type AsyncVoidFn<TParams> = Async<VoidFn<TParams>>
/**
* Create a version of a function that may either be sync or async
*/
export type SyncOrAsync<TFn extends (...args: any[]) => any> = (
...args: Parameters<TFn>
) => ReturnType<TFn> | Promise<ReturnType<TFn>>
/**
* Create a version of `Fn` that may either be sync or async
*/
export type SyncOrAsyncFn<TParams, TResult> = SyncOrAsync<Fn<TParams, TResult>>
/**
* Create a version of `OptionFn` that may either be sync or async
*/
export type SyncOrAsyncOptionFn<TParams, TResult> = SyncOrAsync<
OptionFn<TParams, TResult>
>
/**
* Create a version of `VoidFn` that may either be sync or async
*/
export type SyncOrAsyncVoidFn<TParams> = SyncOrAsync<VoidFn<TParams>>
/**
* Returns a function with the arguments reversed
*/
export type ReverseArguments<T extends (...args: any[]) => any> = (
...args: Reverse<Parameters<T>>
) => ReturnType<T>
Arrays
/**
* An array with at least 1 item in it
*/
export type NonEmptyArray<T> = [T, ...T[]]
/**
* Array filter type, used to filter an array via the `.filter` method
*/
export type ArrayFilter<T> = (value: T, index: number, array: T[]) => boolean
/**
* Array map type, used to map an array via the `.map` method
*/
export type ArrayMap<T, U> = (value: T, index: number, array: T[]) => U
/**
* Array reduce type, used to reduce an array via the `.reduce` method
*/
export type ArrayReducer<T, U> = (
previousValue: U,
currentValue: T,
currentIndex: number,
array: T[],
) => U
/**
* Defines a step type that may contain different data types as an array such
* that each step adds or removes from the current input but should always
* result in the same final object that can be validated by just verifying the
* final length assuming all steps have been validated prior
*/
export type StepsOf<Final extends Readonly<any[]>> = Final extends [
...infer Next,
infer _,
]
? StepsOf<Next> | Final
: []
/**
* Reverses the input tuple
*/
export type Reverse<T extends Array<unknown>> = T extends [
infer First,
...infer Rest,
]
? [...Reverse<Rest>, First]
: []
/**
* Remove first value from array
*/
export type Eat<T extends unknown[]> = T extends [infer _, ...infer R]
? R
: never
/**
* Remove last value from array
*/
export type EatLast<T extends unknown[]> = T extends [...infer R, infer _]
? R
: never
/**
* Gets all possible indexes of A as an array
*/
export type Indexes<A extends any[]> = A extends [unknown, ... infer I] ? [...Indexes<I>, I['length']] : []
/**
* Gets all possible indexes of A as a union
*/
export type Index<A extends any[]> = Indexes<A>[number]
/**
* Exclude the given T from Arr
*/
export type ArrExclude<Arr, T, Acc extends unknown [] = []> =
Arr extends [infer F, ...infer R]
? T extends F
? ArrExclude<R, T, Acc>
: ArrExclude<R, T, [...Acc, F]>
: Acc
Objects
/**
* Definition for a type guard that checks if a value is of a specific type
*/
export type Guard<TResult, TOther = unknown> = (
value: TResult | TOther,
) => value is TResult
/**
* Create a type where the provided keys are optional
*
* @param T the base type
* @param O the keys to make optional
*/
export type WithOptional<T extends {}, O extends keyof T> = Omit<T, O> &
Partial<Pick<T, O>>
/**
* Create a type where the provided keys are required
*
* @param T the base type
* @param R the keys to make required
*/
export type WithRequired<T extends {}, R extends keyof T> = T &
Required<Pick<T, R>>
/**
* Create a type where all all properties and sub-properties are recursively partial unless they are
* of the type specified in TKeep
*
* @param T the base type
* @param TKeep types to not make partial
*/
export type DeepPartial<T, TKeep = never> = T extends TKeep
? T
: T extends object
? {
[P in keyof T]?: DeepPartial<T[P], TKeep>
}
: T
/**
* Returns a specific subset of `keyof T`
*
* The resulting types can be used with utilities like `Omit` or `Pick` in a reusable manner
*
* @param T the base type
* @param K keys of T
*/
export type Keys<T, K extends keyof T> = K
export type Primitive = string | number | boolean | Symbol | Date
/**
* Create a type where all direct properties are optional if they're Primitive otherwise it will be
* a partial of the property
*
* @param T the base type
* @param TKeep the types to not partialize
*/
export type FlatPartial<T, TKeep = Primitive> = {
[K in keyof T]: T[K] extends TKeep ? T[K] | undefined : Partial<T[K]>
}
/**
* Gets all keys of an object where the property at that key in the object extends a condition
*
* @param Cond the condition that properties need to extend
* @param T the object with the keys of interest
*/
type ConditionalKeys<Cond, T> = {
[K in keyof T]: T[K] extends Cond ? K : never
}[keyof T]
/**
* Get the primitive keys of a given object
*/
type PrimitiveKeys<T> = ConditionalKeys<Primitive, T>
/**
* Pick the keys of an object where the property value is a primitive
*/
type PickPrimitive<T> = Pick<T, PrimitiveKeys<T>>
/**
* Pick the keys of an object where the property value is not primitive
*/
type ObjectKeys<T> = Exclude<keyof T, PrimitiveKeys<T>>
/**
* Join two keys using a separator
*/
type JoinKeys<
TSep extends string,
P extends string,
K extends string,
> = `${P}${TSep}${K}`
/**
* Create an object with all keys prepended with some prefix
*
* @param T the type with the keys of interest
* @param P the prefix to prepend keys with
* @param Sep the separator to use for nesting keys
*/
type ExpandPathsRec<T, P extends string, Sep extends string> = {
[K in keyof T & string]: T[K] extends Primitive
? {
[key in JoinKeys<Sep, P, K>]: T[K]
}
: ExpandPathsRec<T[K], JoinKeys<Sep, P, K>, Sep>
}[keyof T & string]
/**
* Create the resultant nested object using the given keys of the input object
*
* @param T the object to un-nest
* @param P the keys to keep when un-nesting
*/
type ExpandPaths<T, P extends keyof T, Sep extends string> = P extends string
? ExpandPathsRec<T[P], P, Sep>
: {}
/**
* Bring all object properties to the top-level recursively
*
* @param T the object to expand
* @param Sep the separator to use when expanding
*/
type Pathify<T, Sep extends string> = PickPrimitive<T> &
ExpandPaths<T, ObjectKeys<T>, Sep>
/**
* Get object with only keys that are an array
*/
export type PickArrays<T> = {
[K in keyof T as T[K] extends Array<any> ? K : never]: T[K]
}
/**
* Get the keys of the object `Obj` that start with the given string `P`
*
* @param P the string that a key should be prefixed with
* @param Obj the object from which to take the matching keys
*/
export type KeysStartingWith<P extends string, Obj> = keyof Obj &
`${P}${string}`
/**
* Get the keys of the object `Obj` that end with the given string `S`
*
* @param S the string that a key should be suffixed with
* @param Obj the object from which to take the matching keys
*/
export type KeysEndingWith<S extends string, Obj> = keyof Obj & `${string}${S}`
/**
* Get a type or else a fallback if it is `never`
*
* @param T the base type
* @param F the fallback to use if `T extends never`
*/
export type OrElse<T, F> = T extends never ? F : T
/**
* Check if two types are exactly equal
*
* This is a direct copy from type-challenges:
* https://github.com/type-challenges/type-challenges/blob/main/utils/index.d.ts
*/
export type Equal<X, Y> =
(<T>() => T extends X ? 1 : 2) extends
(<T>() => T extends Y ? 1 : 2) ? true : false
Strings
import { NonEmptyArray, Reverse } from './arrays'
/**
* Types that can be cleanly/predictably converted into a string
*/
export type Stringable = Exclude<string | number, ''>
/**
* String type that will show suggestions but accept any string
*
* @param TSugg the strings that you want to appear as IDE suggestions
*/
export type SuggestedStrings<TSugg> = TSugg | (string & {})
/**
* Joins stringable members into a single, typed, string
*/
export type Join<TSep extends string, T extends Array<Stringable>> = T extends [
infer El,
...infer Rest,
]
? El extends Stringable
? Rest extends NonEmptyArray<Stringable>
? `${El}${TSep}${Join<TSep, Rest>}`
: El
: ''
: ''
/**
* Split a strongly typed string into its parts
*/
export type Split<
TSep extends string,
TStr extends string,
TBase extends string[] = [],
> = TStr extends `${infer El}${TSep}${infer Rest}`
? [...TBase, El, ...Split<TSep, Rest>]
: [TStr]
/**
* Get the chars of a strongly typed string as an array
*/
export type Chars<
TStr extends string,
TBase extends string[] = [],
> = TStr extends `${infer El}${''}${infer Rest}`
? Rest extends ''
? [TStr]
: [...TBase, El, ...Chars<Rest>]
: never
/**
* Get the length of a string
*/
export type Length<TStr extends string> = Chars<TStr>['length']
/**
* Get the first character of a string
*/
export type First<T extends string> = T extends `${infer F}${string}`
? F
: never
/**
* Get the last character of a string
*/
export type Last<TStr extends string> = Reverse<Chars<TStr>>[0]
/**
* Get a string with the first character omitted
*/
export type Eat<T extends string> = T extends `${string}${infer E}` ? E : never
JSON Schema
Type for inferring a simple JSON Schema into a TypeScript type. I think this will become super useful once Typescript adds support for importing JSON values as const. This will mean that using the schema would be something like:
// currently the `const` part doesn't exist, but would be super cool if we could get here
import schema from './my-schema.json' with { type: 'json', const: true };
export type MySchema = FromJSONSchema<typeof schema>;
The open issue that would make the above possible is this: https://github.com/microsoft/TypeScript/issues/32063
It's relatively trivial to implement some kind of pre-build script that could do this for all JSON schemas but I think it's worth waiting till the TS part of this solution exists because it would be great to be able to infer this statically
/**
* Relevant schema structure for inferring type definition
*/
interface PartialJSONSchema<T extends Properties = Properties> {
type: 'object'
properties: T
required?: Array<keyof T>
}
type TypeMap = {
string: string
boolean: boolean
}
type Type = TypeMap[keyof TypeMap]
type Properties = Record<string, { type: keyof TypeMap; default?: unknown }>
/**
* Create a type where the provided keys are required
*
* @param T the base type
* @param R the keys to make required
*/
type WithRequired<T, R extends keyof T> = T & Required<Pick<T, R>>
/**
* Keys that have been defaulted by the
*/
type DefaultedKeys<T extends Properties> = {
[K in keyof T]: T[K]['default'] extends Type ? K : never
}[keyof T]
/**
* A field is required if its default value is in the list of required keys
*/
type Structure<T extends PartialJSONSchema> = WithRequired<
{
[K in keyof T['properties']]?: TypeMap[T['properties'][K]['type']]
},
DefaultedKeys<T['properties']>
>
/**
* Extracts inner resultant type to make IDE inferrence/display better
*/
type Simplify<T> = {
[KeyType in keyof T]: T[KeyType]
}
/**
* Get the typescript type inferred by a JSON Schema
*/
export type FromJSONSchema<T extends PartialJSONSchema> = Simplify<
T['required'] extends Array<keyof T['properties']>
? WithRequired<Structure<T>, T['required'][number]>
: Structure<T>
>