Skip to content

Interpolated Key Names in Mapped ObjectsΒ #39350

Closed
@bradennapier

Description

@bradennapier

Search Terms

Suggestion

So I run into this quite a bit. While usually you can just work around this and type generally, I think enough libraries have situations where this makes sense that it would make for a powerful feature. Essentially the idea is to allow something similar to template strings when defining a type.

I am sure this potentially may have implications that are larger than I am picturing, but the issue comes up whenever a library provides the ability to use strings in unconventional ways.

Use Cases

For example, the Intercom API defines the ability to search contacts by location.country or custom_attributes.${attribute_name}. Potentially if thought out further this could also be expanded to properly handling cases where a string like path.to.key is used in functions such as lodash or similar - but potentially the ability to use this simply opens the door to being type safe across many other interfaces and apis.

Starting with simple interpolation, perhaps including via (or exclusively during) mapped objects.

This seems like it theoretically wouldn't be that difficult to implement (just allow interpolation during the mapping of keys) and would fix a number of situations that would otherwise not be type safe at all.

I've run into quite a few cases where this pattern would provide a solution, although I am having a hard time remembering exactly. I know there were cases in React, Lodash, ElasticSearch and a number of others. If others have examples it would be great if you could comment as well. I will update as I find the other examples where this would fix.

Examples

Basic Example - Replacement / Substitution

type Example = {
  one: string;
  [`some.*`]: any
}

The above would solve the simple cases where we dont know how to define the types of keys that start this way but only those can be any rather than needing to change the type in this case to { [key: string]: any }

Most Powerful Option - Full Interpolation

There are a number of considerations:

Given the following for Contact model:

// shortened a bit for simplicity
export type Contact<
  ATTR extends { [key: string]: any }
> = {
  external_id: string;
  /** The contact's email */
  email: string;
  /** An object showing location details of the contact. */
  location: Location;
  tags: Tag[];
  segments: Segment[];
  /** The custom attributes which are set for the contact. */
  custom_attributes: ATTR;
};

Now if we need define the type Intercom allows searching by a number of keys plus some interpolated keys potentially, one of these is custom_attributes.${key} and the other is location.${key}:

It would seem the best way to do this and perhaps exclusively allowed way would be with mapped objects:

type SearchableAttributes<O extends { [key: string]: any }> = {
  [`custom_attributes.${K in keyof O}`]: O[K]
}

type SearchableLocation = {
  [`location.${K in keyof Location}`]: Location[K]
}

type SearchableContact<
  ATTR extends { [key: string]: any }, 
  C extends Contact<ATTR> = Contact<ATTR>
> = 
  Omit<C, 'custom_attributes' | 'location' | 'tags' | 'segments'> 
  & SearchableAttributes<C['custom_attributes']> 
  & SearchableLocation 
  & {
      tag_id: Tag['id'];
      segment_id: Segment['id'];
   };

Potentially even allowing a extendable version of this, but would probably be a bigger change

type MappedProperty<PREFIX extends string, O extends { [key: string]: any }> = {
   [`${PREFIX}.${K in keyof O}`]: O[K]
}

Since the above would probably need to indicate that string must be const this may be more difficult...

type MappedProperty<PREFIX extends const, O extends { [key: string]: any }> = {
   [`${PREFIX}.${K in keyof O}`]: O[K]
}

type SearchableLocation = MappedProperty<'location', Contact['location]>

Checklist

My suggestion meets these guidelines:

  • This wouldn't be a breaking change in existing TypeScript/JavaScript code
  • This wouldn't change the runtime behavior of existing JavaScript code
  • This could be implemented without emitting different JS based on the types of the expressions
  • This isn't a runtime feature (e.g. library functionality, non-ECMAScript syntax with JavaScript output, etc.)
  • This feature would agree with the rest of TypeScript's Design Goals.

Metadata

Metadata

Assignees

No one assigned

    Labels

    DuplicateAn existing issue was already created

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions