I made a small type alias for this question:
given a value of type
T, what strings could JStypeof valuereturn at runtime?
Not proposing it for TS itself. I know the team does not take new utility types. I just want feedback on whether this seems useful.
export type Typeof<T> = void extends T // explicitly treat void as a top type to account for lambda returns
? 'object' | 'function' | 'string' | 'number' | 'bigint' | 'boolean' | 'symbol' | 'undefined'
: TypeofType<T, undefined, 'undefined'> | TypeofType<T, null, 'object'> | TypeofImpl<T & {}>;
type TypeofImpl<T> = T extends Function // all callable objects are functions
? 'function'
: TypeofType<T, string, 'string'>
| TypeofType<T, boolean, 'boolean'>
| TypeofType<T, number, 'number'>
| TypeofType<T, symbol, 'symbol'>
| TypeofType<T, bigint, 'bigint'>
| TypeofType<T, Function, 'function'>
| TypeofType<T, object, 'object'>;
type TypeofType<A, B, S> = A extends B ? S : B extends A ? S : never;
Examples:
Typeof<string> // "string"
Typeof<null> // "object"
Typeof<() => void> // "function"
Typeof<string | number> // "string" | "number"
Typeof<{ a: 1 } | null> // "object"
What it helps with: plain typeof value in generic code usually widens to the full JS typeof union, even when T rules out most cases. Typeof<T> keeps that result correlated with T.
const typeOf = <T>(value: T) => typeof value as Typeof<T>;
That makes things like these possible:
const isTypeof = <T>(value: T, expected: Typeof<T>) => typeof value === expected;
declare const x: string | number | null;
isTypeof(x, 'string'); // ok
isTypeof(x, 'number'); // ok
isTypeof(x, 'object'); // ok
isTypeof(x, 'function'); // error
and:
type EventPayload = string | bigint | { id: string } | (() => void);
type DispatchKey = Typeof<EventPayload>;
// "string" | "bigint" | "object" | "function"