Question

Get Keys of a TypeScript Interface as an Array of Strings

typescriptjavascript

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:

  1. Get the keys as a type
    • Use keyof IMyTable
    • This gives a type like:
type MyTableKeys = keyof IMyTable;
// "id" | "title" | "createdAt" | "isDeleted"
  1. 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 interface describes 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.

Take Quiz

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:

  • myTableKeys must 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);
}
Take Quiz

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:

  • IMyTable is 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

ApproachRuntime value?Type-safe?Best use case
interface IMyTable { ... }NoYesDescribing object shapes
keyof IMyTableNoYesRepresenting allowed property names as a type
(keyof IMyTable)[]YesYesKeeping a real array of valid keys
Object.keys(realObject)YesPartlyGetting keys from an actual object
Schema object + keyof typeofYesYesWhen you need both runtime metadata and types

interface vs schema object

FeatureInterfaceSchema object
Exists at runtimeNoYes
Can be iteratedNoYes
Gives compile-time checksYesYes
Good for metadataNoYes

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.
Take Quiz

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

  • interface exists only at compile time
  • keyof creates a type, not an array
  • use a real object or array for runtime operations
  • (keyof T)[] gives type-checked string keys
  • Object.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.

Take Quiz

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.
Take Quiz

Keep learning

Related questions

View more