Question

TypeScript Object Indexing with String Keys and keyof

typescriptjavascript

Question

I am converting a JavaScript DNA-to-RNA transcriber to TypeScript. The code splits an input string into characters and uses each character to look up a replacement value in an object.

class Transcriptor {
  DNATranscriber = {
    G: "C",
    C: "G",
    T: "A",
    A: "U"
  };

  toRna(sequence: string) {
    const sequenceArray = [...sequence];
    const transcriptionArray = sequenceArray.map((character) => {
      return this.DNATranscriber[character];
    });
  }
}

export default Transcriptor;

TypeScript reports the following error:

TS7053: Element implicitly has an 'any' type because expression of type 'string' can't be used to index type '{ G: string; C: string; T: string; A: string; }'.
No index signature with a parameter of type 'string' was found on type '{ G: string; C: string; T: string; A: string; }'.

Why does this happen even when the object keys are written as strings, and how can the lookup be typed safely without using any?

Short Answer

TypeScript distinguishes between an object with a known, finite set of keys and an object that accepts every possible string key. You will learn why a string cannot automatically index a DNA mapping, how keyof represents valid keys, and how runtime validation makes dynamic lookups safe.

Concept

TypeScript infers the mapping as an object with four specific properties:

{
  G: string;
  C: string;
  T: string;
  A: string;
}

Its valid keys are the union:

type DnaBase = "G" | "C" | "T" | "A";

However, spreading a value typed as string produces characters that are also typed as string:

const sequence: string = "ACGT";
const character: string = [...sequence][0];

A string could be "A", but it could also be "X", "1", or an empty value in another situation. TypeScript cannot safely promise that every possible string is a key in the mapping.

Writing keys with quotation marks does not change this. In JavaScript, G: "C" and "G": "C" create the same string-named property. The problem is not the key format; it is that the lookup value is broader than the set of known keys.

This check prevents bugs such as looking up an invalid DNA base and silently receiving undefined.

Mental Model

Think of the transcriber as a cabinet with exactly four labelled drawers: A, C, G, and T.

A value of type DnaBase is a request that names one of those four drawers, so TypeScript allows it.

A value of type string is like a request with any possible label, including X. Since the cabinet has no X drawer, TypeScript asks you to either:

  • verify the label before opening a drawer, or
  • declare that the cabinet is intended to accept every string label.

For DNA transcription, verifying the label is usually the more accurate design.

Take Quiz

Syntax and Examples

Use keyof typeof to derive a union of valid keys from an object, then use a type guard to validate input characters.

const DNA_TRANSCRIBER = {
  G: "C",
  C: "G",
  T: "A",
  A: "U"
} as const;

type DnaBase = keyof typeof DNA_TRANSCRIBER;

function isDnaBase(value: string): value is DnaBase {
  return Object.prototype.hasOwnProperty.call(DNA_TRANSCRIBER, value);
}

function toRna(sequence: string): string {
  return [...sequence]
    .map((character) => {
      if (!isDnaBase(character)) {
        throw new Error(`Invalid DNA base: ${character}`);
      }

      return DNA_TRANSCRIBER[character];
    })
    .join("");
}

console.log(toRna("ACGT")); // "UGCA"

keyof typeof DNA_TRANSCRIBER becomes "G" | "C" | "T" | "A" automatically. The type guard checks the value at runtime and tells TypeScript that, after a successful check, character is one of those valid keys.

The as const preserves the exact output values ("C", "G", "A", and "U"). It is useful when you want the most precise inferred types, though the key-indexing solution also works without it.

Step by Step Execution

Consider this input:

const result = toRna("ATG");

Execution proceeds as follows:

  1. [..."ATG"] creates ['A', 'T', 'G'].
  2. The first character is "A".
  3. isDnaBase("A") checks whether DNA_TRANSCRIBER owns an A property and returns true.
  4. TypeScript now treats the character as DnaBase, not merely string.
  5. DNA_TRANSCRIBER["A"] returns "U".
  6. The next lookups return "A" for "T" and "C" for "G".
  7. Joining the results produces "UAC".

For invalid input, validation stops the conversion:

toRna("AXG");
// Error: Invalid DNA base: X

This is preferable to returning a string containing an accidental missing value.

Real World Use Cases

Finite-key object lookups appear frequently in applications:

  • Status labels: map API values such as "pending" and "paid" to display text.
  • HTTP status handling: map a known set of status codes to messages.
  • Configuration: retrieve settings by a known configuration key.
  • Form fields: access values only after verifying a submitted field name.
  • Localization: map a known translation key to translated text.
  • Parsing and conversion: translate tokens, units, DNA bases, or command names.

Whenever a key comes from user input, a file, an API, or another dynamic source, validate it before using it to access a finite mapping.

Take Quiz

Real Codebase Usage

In real projects, choose the type strategy that matches the data contract.

Validate untrusted strings at the boundary

Functions receiving text from users, APIs, or files should normally accept string and validate it immediately.

function getBasePair(character: string): string {
  if (!isDnaBase(character)) {
    throw new Error("Expected A, C, G, or T");
  }

  return DNA_TRANSCRIBER[character];
}

This is a guard-clause pattern: reject invalid input early, then keep the rest of the function simple and type-safe.

Use a narrow type for already validated data

If another layer has already validated the sequence, model that fact in the parameter type.

function transcribeBase(base: DnaBase): string {
  return DNA_TRANSCRIBER[base];
}

This function cannot be called with "X" without an explicit unsafe assertion.

Use Record<string, Value> only for truly open dictionaries

For data where any string key is allowed, use a string index signature or Record<string, string>:

const headers: Record<string, string> = {
  "content-type": "application/json"
};

headers["x-request-id"] = "abc123";

That model is appropriate for open-ended headers, metadata, or caches—not for a mapping that only supports four DNA bases.

Common Mistakes

Adding quotes to property names

These declarations are equivalent for ordinary identifier-like keys:

const one = { A: "U" };
const two = { "A": "U" };

Quotes do not create an index signature or make arbitrary string indexing safe.

Silencing the error with any

const value = (this.DNATranscriber as any)[character];

This removes TypeScript's protection. A lookup for "X" returns undefined, but TypeScript will no longer warn you. Prefer validation or a correctly typed dictionary.

Using an unchecked type assertion

return this.DNATranscriber[character as DnaBase];

This compiles, but it does not validate the value at runtime. Use an assertion only when your program has already guaranteed the value is valid.

Using Record<DnaBase, string> and expecting arbitrary strings to work

type DnaBase = "A" | "C" | "G" | "T";
const map: Record<DnaBase, string> = { A: "U", C: "G", G: "C", T: "A" };

const input: string = "A";
map[input]; // Error: input may be "X", not only a DnaBase

Record<DnaBase, string> ensures all DNA keys exist. It does not mean every string is valid.

Forgetting to return the result

The original TypeScript method creates an array but does not return the RNA string. End the method with:

return transcriptionArray.join("");

Ignoring undefined in open dictionaries

With noUncheckedIndexedAccess enabled, indexing a Record<string, string> can produce string | undefined, because a requested key might not exist. Handle the missing case instead of assuming a value exists.

Comparisons

ApproachKey type accepted for lookupBest useMain trade-off
Specific object + keyofOnly known keys such as "A" | "C" | "G" | "T"Fixed mappingsDynamic strings must be narrowed first
Type guard + keyofAny input string after runtime validationUser input, API data, parsingRequires a validation branch
Record<DnaBase, string>DnaBase onlyRequire every known key in a mapStill does not accept arbitrary string
Record<string, string>Any stringOpen-ended dictionariesInvalid/missing keys can be overlooked
as anyAnythingAvoid in application codeDisables useful type checking

A finite mapping represents a closed set of keys. A Record<string, string> represents an open dictionary. Pick the one that matches the actual rules of your data.

Take Quiz

Cheat Sheet

const map = {
  A: "U",
  C: "G",
  G: "C",
  T: "A"
} as const;

type Key = keyof typeof map;
// "A" | "C" | "G" | "T"
function isKey(value: string): value is Key {
  return Object.prototype.hasOwnProperty.call(map, value);
}

function lookup(value: string) {
  if (!isKey(value)) throw new Error("Invalid key");
  return map[value];
}
  • obj[key] is safe when key is assignable to keyof typeof obj.
  • Quoted object keys are still string property names; quotes do not add an index signature.
  • Use Record<Key, Value> for a required, finite set of keys.
  • Use Record<string, Value> only when arbitrary string keys are valid.
  • Do not use any merely to bypass an indexing error.
  • A type assertion changes compile-time checking, not runtime data.

FAQ

Why can't I use a string to index a TypeScript object?

A string can contain any value, while an object may define only a limited set of property names. TypeScript prevents a lookup that could request a missing property.

What does keyof typeof object mean?

typeof object obtains the object's type, and keyof extracts its property names as a union. For the DNA map, it produces "A" | "C" | "G" | "T".

Do quoted keys create a string index signature?

No. { A: "U" } and { "A": "U" } have the same relevant key type. A string index signature must be declared explicitly, such as Record<string, string>.

Should I use Record<string, string> to fix TS7053?

Only if every possible string is a meaningful key in your application. For DNA bases, invalid values should be rejected, so a type guard and finite key union are safer.

Is character as keyof typeof map safe?

Not by itself. It tells TypeScript to trust you but does not check the real input. Validate the character first unless it is already guaranteed to be valid.

Why does annotating with any remove the error?

any opts out of type checking for that value. The code may compile, but an invalid key can still return undefined at runtime.

Can a class use the same pattern?

Yes. Define a readonly mapping property, derive a key type from a shared constant, and validate the input before indexing the property.

Take Quiz

Mini Project

Description

Build a DNA-to-RNA conversion utility that accepts a sequence from an external source. The utility must validate each character before using it as a key in a fixed transcription table, preventing invalid input from becoming undefined in the output.

Goal

Create a TypeScript class that safely converts valid DNA sequences to RNA and throws a clear error for invalid bases.

Requirements

Requirement 1

Take Quiz

Keep learning

Related questions

View more