Typing Built-in Objects in TypeScript: Math, Date, RegExp, and More
Introduction
Working with built-in JavaScript objects like Date, RegExp, and Math is everyday work for intermediate TypeScript developers. These objects are provided by the runtime and have well-known APIs, but when you bring TypeScript's type system into the mix, subtle issues appear: runtime values that look like Dates but are strings from JSON, RegExp flags passed as arbitrary strings, or utility wrappers that return unions of built-in types. This article explains how to type these built-ins precisely, how to write reliable runtime guards, how to handle serialization and parsing, and how to extend or wrap built-ins without losing type safety.
By the end of this tutorial you'll understand how to:
- Use TypeScript's existing types for built-ins and when to create wrapper types
- Write precise type guards for Date and RegExp and avoid common pitfalls
- Deal with serialized data (JSON) that contains built-ins and recover strong types
- Extend or augment global declarations safely
- Apply advanced patterns like branded types, narrow literals with as const, and safe runtime validation
This is an in-depth, pragmatic guide with code examples, patterns, troubleshooting tips, and links to deeper reading for related TypeScript topics.
Background & Context
Built-in objects often sit at the boundary between runtime behavior and static types. TypeScript ships with DOM and ES lib types that describe Date, RegExp, and Math. However, those types don't cover runtime conversions (like parsing ISO date strings from APIs), or developer APIs that accept either a Date object or an ISO string. Mistakes here lead to runtime errors and brittle code. Good typing prevents accidental misuse, improves editor UX, and makes refactors safer.
We will combine TypeScript static features (unions, type guards, branded types, const assertions) with runtime checks (instanceof, typeof, and validation) to produce robust code. You'll also see patterns used for asynchronous parsing and error typing when promises reject with specific built-ins.
Key Takeaways
- Type built-ins precisely using existing TypeScript lib types when possible
- Use runtime type guards (instanceof, typeof) to narrow built-ins safely
- Prefer branded or opaque wrapper types when you need stronger guarantees than raw built-ins
- Handle JSON and external payloads explicitly—don't assume runtime shapes
- Use const assertions and the satisfies operator to retain literal types for flags and options
- Avoid augmenting global types unnecessarily; prefer local interfaces or declared wrappers
Prerequisites & Setup
To follow along you should have:
- TypeScript 4.5+ (some examples use features available in later releases; for literal inference you can use 4.9+ for the satisfies operator)
- Node.js 14+ or modern browser environment to run examples
- Familiarity with TypeScript basics: unions, generics, type guards, and declaration merging
Editor tips: enable "strict" in tsconfig.json to make types more effective. If you plan to validate runtime data, include a lightweight validator like zod or use custom guards.
Main Tutorial Sections
1) Using the Built-in Types Correctly
TypeScript already includes types for Date, RegExp, and Math. Use them as the canonical types instead of any or object:
function formatDate(d: Date): string {
return d.toISOString();
}
function testRegex(r: RegExp, s: string) {
return r.test(s);
}
// Math is available as a global value with well-known static methods
const pi = Math.PI; // typed as numberAvoid using "object" or "any" for parameters you'll call methods on; prefer the built-in types so you get completions and compiler checks.
2) Writing Safe Type Guards for Date and RegExp
instanceof is the simplest guard for Date and RegExp in most environments:
function isDate(v: unknown): v is Date {
return v instanceof Date && !Number.isNaN(v.getTime());
}
function isRegExp(v: unknown): v is RegExp {
return v instanceof RegExp;
}Note: when values cross worker or iframe boundaries, instanceof can fail; in those cases check constructor name or duck-type methods (has getTime, test, source).
3) Handling Serialized Dates from JSON
APIs commonly send dates as ISO strings. Naively typing an API response as { createdAt: Date } will be wrong until you parse it. Use runtime revivers or conversion helpers.
type ApiPayload = { id: string; createdAt: string };
function parsePayload(raw: ApiPayload) {
return { id: raw.id, createdAt: new Date(raw.createdAt) } as { id: string; createdAt: Date };
}For safer parsing, use a reviver with JSON.parse:
const obj = JSON.parse(jsonString, (k, v) => {
if (typeof v === 'string' && isoDateRegex.test(v)) return new Date(v);
return v;
});Consider linking to deeper reading on typing external JSON payloads when you design API clients: Typing JSON Payloads from External APIs (Best Practices).
4) Branded and Opaque Types for Distinguishing Dates and Strings
To prevent confusing raw strings with parsed Date objects at compile time, use a branded string type for serialized date formats:
type ISODateString = string & { __brand: 'iso-date' };
function toISO(d: Date): ISODateString {
return d.toISOString() as ISODateString;
}
function parseISO(s: ISODateString): Date {
return new Date(s);
}Now a function expecting ISODateString won't accept arbitrary strings, preventing accidental misuse.
5) Typing RegExp Creation and Flags
When you build RegExp objects dynamically, validate flags and patterns. You can preserve literal flag types via const assertions and the satisfies operator to keep IDE hints:
const flags = 'gi' as const; // inferred as 'gi'
function makeRegex<P extends string, F extends string>(p: P, f: F) {
return new RegExp(p, String(f));
}
// Using satisfies (TS 4.9+) for describing allowed flags object
const allowed = { flags: 'g' } as const;For more on when to use as const and literal inference, see When to Use const Assertions (as const) in TypeScript and Using as const for Literal Type Inference in TypeScript.
6) Working with Math-like APIs and Namespaces
Math is a global object with static methods; you usually call these directly. If you need a Math-like dependency injection for testability, define a MathLike interface and accept it via parameter injection:
interface MathLike {
abs(x: number): number;
random(): number;
floor(x: number): number;
}
function compute(m: MathLike = Math) {
return m.floor(m.random() * 100);
}This pattern allows you to pass mocks in tests while preserving type safety.
7) Extending Prototypes vs Wrapping
Augmenting built-in prototypes (e.g., adding Date.prototype.toFriendlyString) can be tempting, but introduces global, implicit behavior and typing complexity. If you must extend, prefer module augmentation with clear declarations:
export {};
declare global {
interface Date {
toFriendlyString(): string;
}
}
Date.prototype.toFriendlyString = function () {
return this.toISOString().split('T')[0];
};Prefer wrapping instead of mutation when possible: create a small adapter function that accepts Date and returns formatted strings. If you rely on this-context typing in methods, review patterns in Typing Functions with Context (the this Type) in TypeScript.
8) Combining Type Guards and Type Assertions: When to Use Which
Sometimes you must convince the compiler of a type after a runtime check — use type guards rather than assertions. Prefer user-defined type guards for clarity and safety:
function isDateString(s: unknown): s is ISODateString {
return typeof s === 'string' && isoDateRegex.test(s);
}
function handle(v: unknown) {
if (isDate(v)) {
// v is Date
console.log(v.toISOString());
} else if (isDateString(v)) {
const d = new Date(v);
console.log(d.toISOString());
} else {
throw new TypeError('Expected Date or ISODateString');
}
}For more guidance on assertions vs guards vs narrowing see Using Type Assertions vs Type Guards vs Type Narrowing (Comparison).
9) Typing Collections that Contain Built-ins
You often have arrays or objects that mix built-ins: e.g., (Date | string)[] or (RegExp | string)[]. When possible, narrow these to single types before processing using map + parse steps:
const raw: (Date | string)[] = [new Date(), '2023-01-01T00:00:00Z']; const dates: Date[] = raw.map(x => (isDate(x) ? x : new Date(x)));
If the array genuinely needs mixed types, use union types and document the expected runtime cases. For broader patterns on mixed-type arrays, see Typing Arrays of Mixed Types (Union Types Revisited).
10) Asynchronous APIs and Built-ins: Promise Typing
When async functions return built-ins or parse them, type the promise precisely. If a function can return either a Date or null, reflect that:
async function fetchCreatedAt(id: string): Promise<Date | null> {
const res = await fetch(`/items/${id}`);
const json = await res.json();
if (!json.createdAt) return null;
return new Date(json.createdAt);
}If your promise can reject with specific Error types (e.g., TypeError for invalid input), model those behaviors and handle them with typed guards. For patterns on typing promises that resolve or reject to multiple types, consult Typing Promises That Resolve with Different Types and Typing Promises That Reject with Specific Error Types in TypeScript.
Advanced Techniques
Once you’ve mastered the basics, these advanced techniques help when you need more guarantees:
- Branded / opaque types: create distinct types for serialized vs parsed forms (ISODateString vs Date) to make accidental misuse a compile-time error.
- Satisfies and as const: Use const assertions and the satisfies operator to retain literal types for RegExp flags or option objects; this improves inference and refactors. See Using the satisfies Operator in TypeScript (TS 4.9+).
- Validation pipelines: combine lightweight validators (zod/ runtypes) with branded types to guarantee contract correctness at runtime and reflect it in the type system.
- Declarative converters: write small pure functions that convert from raw API payloads to typed domain objects; these are easy to test and reuse.
- Avoid prototype mutation; prefer small wrapper objects or DI for testability. If you must augment global declarations, do so in a single module and document the changes clearly.
Performance tip: avoid creating many Date objects in hot loops—use numeric timestamps when appropriate and convert once at I/O boundaries.
Best Practices & Common Pitfalls
- Do: Use the built-in Date and RegExp types instead of any/object.
- Do: Validate input from external sources and convert strings to Date explicitly.
- Do: Use user-defined type guards (v is Date) rather than type assertions when narrowing.
- Don't: Rely on instanceof across iframe/worker boundaries; prefer duck-typing or structured validation.
- Don't: Mutate built-in prototypes unless necessary; it makes code harder to reason about and to type across modules.
- Pitfall: Treating JSON dates as Date without conversion leads to runtime errors. Always parse and validate.
- Pitfall: Using stringly-typed flags for RegExp. Prefer literal or branded types (use as const) to avoid runtime invalid flag errors.
- Troubleshooting: If code fails to narrow a union, check the order of your guards and ensure your guard returns a boolean and uses the
v is Typesignature.
If you encounter complex validation needs, revisit the section on external payloads and consider structured validators from libraries covered in our API typing guide: Typing JSON Payloads from External APIs (Best Practices).
Real-World Applications
- API clients: parse ISO date strings into Date objects at the API boundary, then operate exclusively on Date in your domain logic.
- Search/Filtering: compile user-supplied patterns into RegExp carefully by sanitizing inputs and validating flags.
- Libraries: when exposing APIs that accept either Date or string, provide typed overloads or accept a branded type to reduce ambiguity.
- Testing: inject a MathLike or clock object for deterministic tests (avoid Math.random in tests). This supports deterministic unit tests and clearer code.
In broader library design, consider how your decisions affect consumer ergonomics—prefer clear conversion functions over ambiguous union parameters.
Conclusion & Next Steps
Typing built-in objects in TypeScript is about clearly modeling the boundary between runtime behavior and static types. Use built-in types where possible, add guarded conversion for external data, prefer wrappers and DI over prototype mutation, and leverage branded types and const assertions for stricter contracts. Next steps: practice by typing a small API client that parses dates and patterns, and explore advanced validation libraries to tighten runtime guarantees.
For follow-up reading, check the guides on literal inference, type guards, and promise typing linked throughout this article.
Enhanced FAQ
Q: When should I use instanceof Date vs a string check? A: Use instanceof Date when you expect an actual Date instance (e.g., internal domain objects constructed in-process). Use string checks when working with transport data (APIs, JSON). When you might get cross-realm objects (iframes/workers), instanceof can fail; prefer duck-typing (has getTime) or explicit parsing and validation.
Q: How can I type a parameter that accepts either a Date or ISO string? A: Use a union: function acceptDate(d: Date | ISODateString). Provide an implementation that narrows it via a type guard or conversion helper:
function ensureDate(d: Date | ISODateString): Date {
return isDate(d) ? d : new Date(d);
}If you want to prevent callers from accidentally passing arbitrary strings, require a branded ISODateString instead of raw string.
Q: Is it safe to extend Date.prototype? How do I type it? A: Extending prototypes is risky—declaring global augmentation is necessary for typing. Prefer wrapper functions or small adapters. If you must augment, put the declaration in a module with declare global and add the implementation exactly once. Be mindful of library consumers who might not include that declaration file; keep augmentation minimal and documented.
Q: How do I type RegExp flags and avoid invalid flags at runtime? A: Use literal types or const assertions for flags. If flags come from external sources, validate them against allowed characters (gimsuy) before calling new RegExp. You can also expose an options object typed with literal unions for flags and use the satisfies operator to preserve type inference—see Using the satisfies Operator in TypeScript (TS 4.9+) for examples.
Q: What's the best way to parse Dates from JSON safely? A: Treat JSON as untyped input: validate and convert at the boundary. Use JSON.parse with a reviver (careful with performance), or parse specific fields with converters that validate format and return branded types for downstream safety. See our API JSON guide for patterns: Typing JSON Payloads from External APIs (Best Practices).
Q: Should I use type assertions (as Date) after checking a value? A: Prefer user-defined type guards (returns v is Date) to assertions. Type assertions skip checks and can hide mistakes. Guards are more maintainable and give better compiler ergonomics. For guidance, read Using Type Assertions vs Type Guards vs Type Narrowing (Comparison).
Q: How do I represent a typed wrapper that can be either a RegExp or a string pattern? A: Use a discriminated union:
type Pattern = { kind: 'regex'; value: RegExp } | { kind: 'string'; value: string };
function compile(p: Pattern) {
if (p.kind === 'regex') return p.value;
return new RegExp(p.value);
}This makes it explicit which shape you're handling and avoids ambiguity.
Q: How do I handle promises that may resolve to a built-in or reject with specific Error types? A: Model the resolved type accurately (Promise<Date | null>) and handle rejections with try/catch. To communicate rejection types, document behavior or wrap errors in discriminated Error subtypes and use type predicates in catch blocks. For typing patterns around promises with various resolve types and specific rejection types, consult our guides on promise typing: Typing Promises That Resolve with Different Types and Typing Promises That Reject with Specific Error Types in TypeScript.
Q: Any advice for debugging type-narrowing issues?
A: If narrowing isn't working, ensure your guard has the signature function isX(v: unknown): v is X. Verify the guard is reachable before using the narrowed branch, and avoid reassigning the variable to a new name (which can widen the type). Use // @ts-expect-error sparingly — prefer fixing the type flow.
Q: How do I handle complex transformations that must validate objects with nested built-ins? A: Build small, composable converters that validate each nested field and return branded types or throw descriptive errors. Consider a validation library like zod to derive both runtime validators and TypeScript types from a single source of truth. For custom needs, design a pipeline: raw -> validated DTO -> domain model (with Date objects, RegExp instances, etc.).
Further reading and related topics are linked throughout this article and include deep dives on JSON payload typing, literal type inference with as const, and type guard patterns to make your usage of built-ins robust and maintainable.
For related patterns around arrays with mixed built-ins, see Typing Arrays of Mixed Types (Union Types Revisited). If you need to type functions that produce or transform built-in objects asynchronously, the guides on async generators and promise typing are useful: Typing Asynchronous Generator Functions and Iterators in TypeScript and Typing Promises That Resolve with Different Types.
If your code constructs code dynamically or evaluates strings that may produce built-ins, review the cautionary guides for Typing Functions That Use new Function() (A Cautionary Tale) and Typing Functions That Use eval() — A Cautionary Tale to reduce risk.
Lastly, when you need to type complex error flows involving built-ins, our guide on Typing Error Objects in TypeScript: Custom and Built-in Errors can help you design safer rejection handling.
Happy typing—treat built-ins as first-class citizens in your type design and you’ll avoid many runtime surprises.
