Question
I have many Lovefield tables and matching TypeScript interfaces that describe their columns.
For example:
export interface IMyTable {
id: number;
title: string;
createdAt: Date;
isDeleted: boolean;
}
I would like to get the property names of this interface as an array of strings, like this:
const IMyTable = ["id", "title", "createdAt", "isDeleted"];
I cannot directly build an object or array from the interface itself, because the table names are obtained dynamically. I need a way to work with the interface properties and produce an array from them.
How can this be achieved in TypeScript?
Short Answer
By the end of this page, you will understand why a TypeScript interface cannot be iterated at runtime, how to use keyof to represent interface keys at compile time, and the practical patterns developers use when they need a real array of property names.
Concept
TypeScript interfaces exist only at compile time. They help the TypeScript compiler check your code, but they are removed when JavaScript is generated.
That is the key reason this does not work:
interface IMyTable {
id: number;
title: string;
}
After compilation, IMyTable does not exist in JavaScript. So you cannot loop over it, inspect it, or convert it into an array at runtime.
What you can do instead
There are two common needs here:
- Get the keys as a type
- Use
keyof IMyTable - This gives a type like:
- Use
type MyTableKeys = keyof IMyTable;
// "id" | "title" | "createdAt" | "isDeleted"
- Get the keys as a real runtime array
- Create a real object or array in code
- Then type it so TypeScript checks that the keys are valid
Example:
const myTableKeys: (keyof IMyTable)[] = ["id", "title", "createdAt", "isDeleted"];
This gives you a real JavaScript array while still getting type checking.
Why this matters
In real projects, you often need both:
- compile-time safety from TypeScript
- runtime data that JavaScript can actually use
Interfaces solve the first problem. Arrays and objects solve the second. A big part of TypeScript programming is knowing when you are working with types versus runtime values.
Mental Model
Think of an interface like a blueprint for a house.
- The blueprint tells builders what the house should look like.
- But you cannot live inside the blueprint.
- Once the house is built, the blueprint is not the house itself.
In the same way:
- an
interfacedescribes the shape of data - but it is not a real JavaScript value
- so you cannot loop over it or read its keys at runtime
If you want a list of keys, you must create a real object or array, just like building the actual house from the blueprint.
Syntax and Examples
keyof for compile-time keys
Use keyof when you want a type representing all property names of an interface.
interface IMyTable {
id: number;
title: string;
createdAt: Date;
isDeleted: boolean;
}
type MyTableKey = keyof IMyTable;
// "id" | "title" | "createdAt" | "isDeleted"
This is useful for function parameters, constraints, and autocomplete.
A typed array of keys
If you need an actual array, define it manually and let TypeScript validate it.
interface IMyTable {
id: number;
title: string;
createdAt: Date;
isDeleted: boolean;
}
const myTableKeys: (keyof IMyTable)[] = [
"id",
"title",
"createdAt",
"isDeleted"
];
If you make a typo, TypeScript will catch it:
const myTableKeys: (keyof IMyTable)[] = [
"id",
"titel" // Error: not a key of IMyTable
];
Using a helper function
A common pattern is to use a helper so the array stays strongly typed.
const keysOf = <T>() => <K extends keyof T>(keys: K[]) => keys;
interface IMyTable {
id: number;
title: string;
createdAt: Date;
isDeleted: boolean;
}
const myTableKeys = keysOf<IMyTable>()([
"id",
"title",
"createdAt",
"isDeleted"
]);
Getting keys from a real object
If you already have a runtime object, you can use Object.keys().
const myTableShape = {
id: 0,
title: "",
createdAt: new Date(),
isDeleted: false
};
const myTableKeys = Object.keys(myTableShape);
// string[]
If you want better typing, you can cast carefully:
const myTableKeys = Object.keys(myTableShape) as (keyof typeof myTableShape)[];
This works because myTableShape is a real JavaScript object. An interface is not.
Step by Step Execution
Consider this example:
interface IMyTable {
id: number;
title: string;
createdAt: Date;
isDeleted: boolean;
}
const myTableKeys: (keyof IMyTable)[] = ["id", "title", "createdAt", "isDeleted"];
Step by step
1. The interface is declared
interface IMyTable {
id: number;
title: string;
createdAt: Date;
isDeleted: boolean;
}
This tells TypeScript that any IMyTable object must have those four properties.
2. keyof IMyTable is computed by TypeScript
keyof IMyTable
TypeScript turns that into this union type:
"id" | "title" | "createdAt" | "isDeleted"
3. The array is checked against that union
const myTableKeys: (keyof IMyTable)[] = ["id", "title", "createdAt", "isDeleted"];
This means:
myTableKeysmust be an array- every item in the array must be one of the valid keys of
IMyTable
4. The array exists at runtime
Unlike the interface, this array is real JavaScript. You can:
- loop through it
- use it in database code
- map over it
- validate input with it
Example:
for (const key of myTableKeys) {
console.log(key);
}
Output:
id
title
createdAt
isDeleted
5. A typo is caught early
const myTableKeys: (keyof IMyTable)[] = ["id", "titel"];
TypeScript reports an error before the code runs. That is the main advantage of this pattern.
Real World Use Cases
Database column definitions
Your exact case is a common one: defining valid column names for a table.
const userColumns: (keyof IUser)[] = ["id", "email", "createdAt"];
This can be used for:
- selecting fields
- building filters
- validating sort options
- generating query helpers
API field whitelists
When allowing clients to request specific fields, you often keep a list of allowed keys.
const publicProfileFields: (keyof UserProfile)[] = ["id", "name", "avatarUrl"];
Form processing
Forms often map field names to object properties.
const editableFields: (keyof AccountSettings)[] = ["displayName", "timezone", "language"];
CSV export/import
When exporting data to CSV, developers often define a list of columns.
const exportColumns: (keyof Order)[] = ["id", "total", "status", "createdAt"];
Validation and sanitization
You can use a typed key array to check whether incoming data contains only allowed properties.
function isAllowedField(field: string): field is keyof IMyTable {
return myTableKeys.includes(field as keyof IMyTable);
}
Real Codebase Usage
In real codebases, developers usually do not try to extract keys from an interface directly. Instead, they choose one of these patterns.
1. Define a typed constant array
This is the simplest and most common solution.
export interface IMyTable {
id: number;
title: string;
createdAt: Date;
isDeleted: boolean;
}
export const MY_TABLE_KEYS: (keyof IMyTable)[] = [
"id",
"title",
"createdAt",
"isDeleted"
];
Benefits:
- easy to read
- runtime-safe
- checked by TypeScript
2. Use a schema object as the source of truth
Some projects avoid interfaces as the main source and use a real object instead.
export const myTableSchema = {
id: "number",
title: "string",
createdAt: "date",
isDeleted: "boolean"
} as const;
export type MyTableKey = keyof typeof myTableSchema;
export const MY_TABLE_KEYS = Object.keys(myTableSchema) as MyTableKey[];
This is useful when you need both:
- runtime metadata
- compile-time types
3. Guard clauses for field validation
Developers often validate incoming field names before using them.
function ensureValidField(field: string): keyof IMyTable {
if (!MY_TABLE_KEYS.includes(field as keyof IMyTable)) {
throw new Error(`Invalid field: ${field}`);
}
return field as keyof IMyTable;
}
4. Configuration-driven code
In larger projects, field lists are often used in config objects.
const tableConfig = {
sortableFields: ["title", "createdAt"] as (keyof IMyTable)[],
hiddenFields: ["isDeleted"] as (keyof IMyTable)[]
};
This keeps behavior explicit and maintainable.
Common Mistakes
Mistake 1: Expecting an interface to exist at runtime
Broken idea:
interface IMyTable {
id: number;
title: string;
}
console.log(Object.keys(IMyTable));
Why it fails:
IMyTableis a type, not a value- it is removed during compilation
Mistake 2: Confusing keyof with a real array
type MyKeys = keyof IMyTable;
This is only a type union, not something you can loop over.
Wrong expectation:
for (const key of MyKeys) {
// Not possible
}
MyKeys does not exist at runtime.
Mistake 3: Using plain string[] and losing safety
const myTableKeys: string[] = ["id", "title", "wrongName"];
This compiles, but TypeScript cannot protect you from invalid keys.
Better:
const myTableKeys: (keyof IMyTable)[] = ["id", "title"];
Mistake 4: Assuming Object.keys() is perfectly typed
const obj = { id: 1, title: "Hello" };
const keys = Object.keys(obj);
keys is usually typed as string[], not ("id" | "title")[].
You often need a cast:
const keys = Object.keys(obj) as (keyof typeof obj)[];
Mistake 5: Forgetting to keep keys and interface in sync
interface IMyTable {
id: number;
title: string;
createdAt: Date;
}
const myTableKeys: (keyof IMyTable)[] = ["id", "title"];
This is valid, but incomplete. TypeScript checks that keys are valid, not that you included every key.
If you need exact completeness, use a schema object as the single source of truth.
Comparisons
| Approach | Runtime value? | Type-safe? | Best use case |
|---|---|---|---|
interface IMyTable { ... } | No | Yes | Describing object shapes |
keyof IMyTable | No | Yes | Representing allowed property names as a type |
(keyof IMyTable)[] | Yes | Yes | Keeping a real array of valid keys |
Object.keys(realObject) | Yes | Partly | Getting keys from an actual object |
Schema object + keyof typeof | Yes | Yes | When you need both runtime metadata and types |
interface vs schema object
| Feature | Interface | Schema object |
|---|---|---|
| Exists at runtime | No | Yes |
| Can be iterated | No | Yes |
| Gives compile-time checks | Yes | Yes |
| Good for metadata | No | Yes |
Which should you choose?
- Use interface when you only need type checking.
- Use typed key arrays when you need a real list of keys.
- Use a schema object when runtime data and type information must stay together.
Cheat Sheet
interface IMyTable {
id: number;
title: string;
createdAt: Date;
isDeleted: boolean;
}
Get keys as a type
type MyTableKey = keyof IMyTable;
// "id" | "title" | "createdAt" | "isDeleted"
Get keys as a real array
const myTableKeys: (keyof IMyTable)[] = [
"id",
"title",
"createdAt",
"isDeleted"
];
Get keys from a real object
const myTableShape = {
id: 0,
title: "",
createdAt: new Date(),
isDeleted: false
};
const keys = Object.keys(myTableShape) as (keyof typeof myTableShape)[];
Important rules
interfaceexists only at compile timekeyofcreates a type, not an array- use a real object or array for runtime operations
(keyof T)[]gives type-checked string keysObject.keys()works only on real objects
Common pattern
export const MY_TABLE_KEYS: (keyof IMyTable)[] = ["id", "title", "createdAt", "isDeleted"];
Edge case
TypeScript checks that each key is valid, but not that the list includes all keys.
FAQ
Can TypeScript convert an interface into an array of keys automatically?
No. Interfaces are removed during compilation, so there is nothing to inspect at runtime.
Why does keyof not give me a string array?
Because keyof creates a type union such as "id" | "title", not a runtime value.
What is the best way to keep key names type-safe?
Use a real array typed as (keyof MyInterface)[].
Can I use Object.keys() with an interface?
Not directly. Object.keys() requires a real object, not a type.
How do I avoid repeating interface keys manually?
Use a schema object as the source of truth, then derive both the type and the keys from that object.
Does TypeScript check that my key array contains every property?
No. It checks that listed keys are valid, but it does not ensure the array is complete.
When should I use a schema object instead of an interface?
Use a schema object when you need runtime metadata such as field names, validation rules, or database column configuration.
Mini Project
Description
Build a small TypeScript utility for table column management. The idea is to define a table shape, keep a safe list of valid column names, and validate user-provided field names before using them. This demonstrates the difference between type-level keys and real runtime arrays.
Goal
Create a typed list of valid table columns and a function that checks whether a string is a valid column name.
Requirements
- Define an interface for a table with at least four fields.
- Create a runtime array of valid keys typed with
keyof. - Write a function that checks whether a given string is a valid key.
- Loop through the key array and print each column name.
- Show one valid and one invalid field check.
Keep learning
Related questions
@Directive vs @Component in Angular: Differences, Use Cases, and When to Use Each
Learn the difference between @Directive and @Component in Angular, including use cases, examples, and when to choose each.
Accessing Input Value from EventTarget in TypeScript
Learn why EventTarget has no value property in TypeScript and safely read values from HTML input events in Angular applications.
Angular (change) vs (ngModelChange): What’s the Difference?
Learn the difference between Angular (change) and (ngModelChange), when each fires, and which one to use in forms and inputs.