Description
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.