NeuroAgent

Complete Guide: Adding Unknown String Properties in TypeScript

Learn how to add unknown string properties to TypeScript types without type errors. Complete guide with practical solutions for text parsing and dynamic property handling. Master TypeScript flexibility today!

Question

How can I add unknown string properties to a TypeScript type without TypeScript complaining?

Consider this basic TypeScript type:

typescript
type A = {
   id: number;
   name?: string;
   age?: number;
   email: string;
   created: Date;
}

I want to extend this type to allow additional unknown properties with string values. My first attempt was:

typescript
type ExtendedA = A & Record<Exclude<string, keyof A>, string>;

However, TypeScript complains that defined properties are not assignable to string, even though I excluded them from the record.

I also tried:

typescript
type ExtendedA = A & {
  [K in string]: K extends keyof A ? never : string;
}

But TypeScript seems to stop at “never” and doesn’t properly check if the key exists in type A.

The use case is for a text parser that processes known properties and adds unknown ones with raw string values. Is TypeScript capable of handling this scenario?

NeuroAgent

To extend a TypeScript type with unknown string properties while maintaining type safety for known properties, you need to use a different approach than simple intersection types. The issue with your attempts is that TypeScript performs an intersection check across all properties, which causes conflicts between the original type and the record type.

The correct solution is to use a mapped type with conditional types to properly exclude known properties:

typescript
type ExtendedA = A & {
  [K in Exclude<string, keyof A>]: string;
}

However, this still has issues because TypeScript’s string type is too broad. A more robust solution is:

typescript
type ExtendedA = A & {
  [K in Exclude<string, keyof A>]: string;
}

Contents

Basic Solution for Unknown String Properties

The most effective way to allow unknown string properties while maintaining type safety is to use a mapped type with proper exclusion:

typescript
type ExtendedA = A & Record<string, string>;

Wait, this still has the same issue. Let me provide the correct solution:

typescript
type ExtendedA = A & {
  [key: string]: string;
}

No, this still conflicts. The proper solution is more nuanced:

typescript
type ExtendedA = A & {
  [K in keyof A]?: any;
} & {
  [K in Exclude<string, keyof A>]: string;
}

Actually, let me look at the search results more carefully. From the StackOverflow result, I can see:

typescript
type InputType = Omit<Record<string, unknown>, keyof AnotherType>

This suggests the correct approach is:

typescript
type ExtendedA = A & Omit<Record<string, string>, keyof A>;

This should work because it creates a record type with string values, then excludes all the known properties from A, and finally intersects with the original type A.


Understanding the Problem

The issue you’re encountering stems from TypeScript’s strict type checking when working with intersection types. When you write:

typescript
type ExtendedA = A & Record<Exclude<string, keyof A>, string>;

TypeScript performs a full intersection analysis across all properties. Even though you’re trying to exclude known properties with Exclude<string, keyof A>, the compiler still checks property compatibility between A and the Record type, which causes conflicts like id: number vs. string values.

From the TypeScript documentation, we understand that intersection types require all properties to be compatible, which is why your approach fails.


Working with Dynamic Properties

For your text parser use case, here’s a more practical approach:

typescript
type ExtendedA = A & {
  [key: string]: string | undefined;
}

This approach allows any string property while maintaining compatibility with the original type. However, it’s not ideal because it allows undefined values for unknown properties.

A better solution is:

typescript
type ExtendedA = A & {
  [key: string]: string;
}

But this will still cause conflicts with the original type’s properties. The most robust solution is:

typescript
type ExtendedA = A & {
  [key: string]: string;
} & {
  [key in keyof A]?: any;
}

This creates a type that:

  1. Maintains all properties from A
  2. Allows any string properties
  3. Makes all original properties optional to avoid conflicts

Best Practices for Text Parsing

For your text parser use case, consider these best practices:

typescript
type ExtendedA = A & {
  [key: string]: string | undefined;
}

// Usage in parser
function parseText(input: string): ExtendedA {
  const result: Partial<A> = {};
  const unknownProps: Record<string, string> = {};
  
  // Parse known properties
  // ...
  
  // Parse unknown properties
  // ...
  
  return { ...result, ...unknownProps } as ExtendedA;
}

This approach separates known and unknown properties, then combines them at runtime while maintaining type safety.


Alternative Approaches

1. Using Index Signatures with Conditional Types

typescript
type ExtendedA = A & {
  [K in keyof A]?: any;
} & {
  [K in Exclude<string, keyof A>]: string;
}

This approach uses mapped types to properly handle the exclusion of known properties.

2. Using Utility Types

From the search results, we can see that using Omit with Record is a common pattern:

typescript
type ExtendedA = A & Omit<Record<string, string>, keyof A>;

3. Using Partial for Known Properties

typescript
type ExtendedA = Partial<A> & {
  [key: string]: string;
} & {
  [key in keyof A]?: never;
}

This makes known properties optional and prevents conflicts.


Complete Example Implementation

Here’s a complete implementation for your text parser use case:

typescript
type A = {
  id: number;
  name?: string;
  age?: number;
  email: string;
  created: Date;
}

type ExtendedA = A & {
  [key: string]: string | undefined;
}

function parseText(input: string): ExtendedA {
  const result: Partial<A> = {};
  const unknownProps: Record<string, string> = {};
  
  // Parse known properties
  // Example logic:
  if (input.includes('id:')) {
    const idMatch = input.match(/id:(\d+)/);
    if (idMatch) {
      result.id = parseInt(idMatch[1]);
    }
  }
  
  // Parse unknown properties
  const unknownMatches = input.match(/(\w+):([^\s]+)/g);
  if (unknownMatches) {
    unknownMatches.forEach(match => {
      const [key, value] = match.split(':');
      if (!(key in result)) { // Check if it's not a known property
        unknownProps[key] = value;
      }
    });
  }
  
  return { ...result, ...unknownProps } as ExtendedA;
}

// Usage
const parsed = parseText("id:123 name:John customProp:value email:john@example.com");
console.log(parsed.id); // 123
console.log(parsed.customProp); // "value"

This implementation provides a practical solution for your text parsing use case while maintaining type safety and allowing unknown string properties.


Sources

  1. TypeScript Documentation - Utility Types
  2. StackOverflow - Exclude properties from type or interface in TypeScript for an arbitrary object
  3. Reddit - How do i make typescript not ignore properties that i haven’t defined in a type ?
  4. Lloyd Atkinson - Typing Unknown Objects in TypeScript With Record Types

Conclusion

TypeScript is capable of handling your scenario, but requires careful type construction. The key insights are:

  1. Intersection types require property compatibility - Simple intersections will fail due to type conflicts between known and unknown properties.

  2. Use mapped types for dynamic property handling - Mapped types with conditional logic provide the flexibility needed for unknown properties.

  3. Consider runtime separation - For text parsing, separating known and unknown properties at runtime then combining them often works better than complex type constructions.

  4. Type assertions can bridge the gap - When TypeScript’s strict checking is too restrictive, type assertions (as) can help bridge the gap between runtime behavior and type safety.

The most practical solution for your use case is to use Partial<A> for known properties and a simple index signature for unknown properties, then combine them at runtime with a type assertion.