Handle Dynamic Values in TypeScript
Learn how to safely type dynamic values from API responses, JSON parsing, user input, and third-party code. Use unknown, type guards, and runtime validation.
TypeScript dynamic values are values whose shape is not known until the program runs: API responses, JSON.parse results, form inputs, environment variables, local storage, and third-party libraries. TypeScript cannot know their shape at compile time, so you must handle them safely at runtime.
The wrong approach is to use any and hope for the best. The right approach is to use unknown, narrow it with checks, and validate critical data.
Use unknown Instead of any
The unknown type is the type-safe alternative to any. While any lets you access any property without complaint, unknown forces you to prove the type before you use it:
function handleDynamicValue(value: unknown) {
console.log(value.toUpperCase());
}Calling a string method directly on an unknown value fails to compile, since the compiler has no evidence the value actually is a string:
Object is of type 'unknown'.TypeScript refuses to let you call a string method on an unknown value because it might not be a string. Fix it by narrowing:
function handleDynamicValue(value: unknown) {
if (typeof value === "string") {
console.log(value.toUpperCase());
}
}For more on the difference, see Any vs Unknown in TypeScript.
Narrow With typeof and Array.isArray
The typeof operator is the simplest way to check a dynamic value's type at runtime. TypeScript understands these checks as type guards:
function processValue(value: unknown): string {
if (typeof value === "string") {
return value.trim();
}
if (typeof value === "number") {
return value.toFixed(2);
}
if (Array.isArray(value)) {
return value.join(", ");
}
return String(value);
}After each typeof check, TypeScript narrows the value to the specific type inside that branch. You can safely call string methods, number methods, and array methods.
typeof handles the primitives you would expect, such as string, number, boolean, and undefined. Two quirks are worth remembering: typeof null returns "object", and typeof for an array also returns "object" instead of a distinct array label. Use Array.isArray when you specifically need to detect an array.
Narrow Object Shapes With the in Operator
For objects with known property names, use the in operator to check for the presence of a key:
type User = { name: string; email: string };
type Product = { title: string; price: number };
function describe(item: User | Product): string {
if ("name" in item) {
return `User: ${item.name}`;
}
return `Product: ${item.title}, $${item.price}`;
}TypeScript narrows the item parameter to User when the name check is true, because Product does not have a name property at all.
This pattern works well with discriminated unions. If each variant has a unique kind or type property, the check is even cleaner:
type ApiResponse =
| { status: "success"; data: unknown }
| { status: "error"; message: string };
function handleResponse(response: ApiResponse): void {
if (response.status === "success") {
console.log("Got data:", response.data);
} else {
console.error("Error:", response.message);
}
}This pattern is called a discriminated union: a common literal field, like the status property here, lets TypeScript distinguish between variants without any extra runtime checks.
Type JSON.parse Results
JSON.parse returns the any type by default, which gives you no type safety. Declare the expected type explicitly:
interface Config {
port: number;
host: string;
}
const rawConfig = '{"port": 3000, "host": "localhost"}';
const config: Config = JSON.parse(rawConfig);
console.log(config.port);The Config annotation tells TypeScript what shape you expect, but this is a compile-time assertion only.
If the JSON string is missing a field or contains extra fields, TypeScript will not catch it. The annotation trusts you.
For safety at the boundary, check the shape at runtime before trusting it. A small helper function keeps that check reusable:
function isConfig(value: unknown): value is Config {
if (typeof value !== "object" || value === null) return false;
const record = value as Record<string, unknown>;
return typeof record.port === "number" && typeof record.host === "string";
}The value is Type return type marks this as a type guard, which the next section covers in more detail. Once the shape is confirmed, parsing the raw string becomes a straightforward check-then-return:
function parseConfig(raw: string): Config {
const parsed: unknown = JSON.parse(raw);
if (!isConfig(parsed)) {
throw new Error("Invalid config format");
}
return parsed;
}This approach verifies the shape at runtime before returning the typed value. For larger objects, a validation library like Zod is more maintainable. See Use Zod with TypeScript.
Create Custom Type Guards
When built-in checks are not enough, write a custom type guard. A type guard is a function whose return type uses the same value-is-Type syntax shown above:
interface Cat {
meow(): void;
}
interface Dog {
bark(): void;
}
function isCat(animal: Cat | Dog): animal is Cat {
return "meow" in animal;
}The animal-is-Cat return type tells TypeScript that when isCat returns true, the value is a Cat. Calling the guard inside an if check narrows the union on both branches:
function handleAnimal(animal: Cat | Dog): void {
if (isCat(animal)) {
animal.meow();
} else {
animal.bark();
}
}For more patterns, see Custom Type Guards in TypeScript.
Handle Dynamic Property Access
JavaScript code often accesses object properties dynamically with bracket notation. TypeScript needs help understanding the value type:
const settings: Record<string, unknown> = {
theme: "dark",
fontSize: 14,
notifications: true,
};
function getSetting(key: string): unknown {
return settings[key];
}
const theme = getSetting("theme");The theme variable is unknown here because Record<string, unknown> tells TypeScript that values could be anything at any key. Narrow it the same way you would narrow any other unknown value before using it:
const themeValue = getSetting("theme");
if (typeof themeValue === "string") {
console.log(themeValue.toUpperCase());
}If you know the exact type for each key, use a more specific type instead. But for truly dynamic key-value stores, like local storage or query parameters, unknown is the honest starting point.
Handle Dynamic Values in Migration
During a JavaScript-to-TypeScript migration, you will encounter places where the code relies on dynamic behavior that is hard to type:
// Legacy JS: merging configs from multiple sources
function buildConfig(envDefaults, fileConfig, cliArgs) {
return Object.assign({}, envDefaults, fileConfig, cliArgs);
}Rather than reaching for any on each parameter, start with Record<string, unknown> for every input object. It keeps the merge honest about not knowing the exact shape yet, while still ruling out primitives like strings or numbers:
function buildConfig(
envDefaults: Record<string, unknown>,
fileConfig: Record<string, unknown>,
cliArgs: Record<string, unknown>
): Record<string, unknown> {
return Object.assign({}, envDefaults, fileConfig, cliArgs);
}This is safe and honest: the function accepts any objects and merges them. As you learn more about the expected shapes, replace the record type with specific interfaces. For details on replacing any systematically, see Replace any During TypeScript Migration.
Avoid These Patterns
Casting with as without checking. Writing JSON.parse(raw) as User lies to TypeScript if the JSON does not match. It is fine for quick prototypes but dangerous in production code.
Using any because unknown feels inconvenient. The inconvenience of unknown is the point. It forces you to think about what the value could be at runtime. Every unknown you narrow is a potential bug you prevented.
Over-narrowing with type predicates. Custom type guards should actually verify the value. Do not write a guard that always returns true just to satisfy the compiler.
Rune AI
Key Insights
- Use unknown, not any, for values whose type is not known at compile time.
- Narrow unknown with typeof, Array.isArray, and the in operator before accessing properties.
- Declare expected types on JSON.parse and API responses with explicit annotations.
- Custom type guards with
value is Typereturn types tell TypeScript what a value is after a check. - For production data, use Zod or a similar runtime validation library that generates TypeScript types.
- The discriminator pattern (a common 'type' or 'kind' field) is the cleanest way to handle different message shapes.
Frequently Asked Questions
Should I use any for values I cannot know at compile time?
How do I type the result of JSON.parse?
What is the quickest way to handle a dynamic value safely?
Conclusion
Dynamic values are the boundary between TypeScript's compile-time safety and the messy reality of runtime data. Use unknown as your default type for anything from outside the codebase. Narrow it with typeof checks, Array.isArray, and the in operator. For critical data, add runtime validation.
More in this topic
TypeScript Compiler Explained
The TypeScript compiler (tsc) turns .ts files into .js files while checking types. Learn what it does, how to use it, and how it fits your workflow.
Set Up ESLint for TypeScript
Install and configure ESLint with typescript-eslint to catch bugs and enforce consistent code style in your TypeScript project.
Pick Utility Type in TypeScript
Pick creates a new type by selecting only the properties you name from an existing type. Use it to build focused types for specific views, API responses, or function parameters.