I have the following interface that defines an object where properties can be of two different types.
export interface OptionsA {
name: string;
}
export interface OptionsB {
parts: number;
}
export interface OptionsConfig {
[key: string]: OptionsA | OptionsB;
}
This works fine, but there is a restriction that properties of type OptionsB
must be prefixed with "@"
.
For example;
const example: OptionsConfig = {
'@sample': {parts: 1},
other: {name: 'example'}
};
So the above works fine, but the following example would be incorrect.
const example: OptionsConfig = {
'@sample': {parts: 1},
other: {name: 'example'},
'@wrong': {name: 'error'}
};
I am wondering if it is possible with TypeScript to declare that @wrong
can only implement the OptionsB
interface, because it has the @
prefix.
Alternatively, is there another approach to achieve a similar kind of restriction.
TL;DR: Copy the first code block 🚀
While this maybe wasn't possible at the time this question was asked, I came across this while I was searching for a solution that I could simply copy & paste. TypeScript 4.1 gave us template literals which make this implementation possible.
Full example on TypeScript playground is here.
First, we need to define some utility types to automatically prefix object types. This can be done with the following code block. If you're not yet familiar with them, I suggest you first read up on template literal and conditional types which are both used heavily in the code below ⬇️
type addPrefix<TKey, TPrefix extends string> = TKey extends string
? `${TPrefix}${TKey}`
: never;
type removePrefix<TPrefixedKey, TPrefix extends string> = TPrefixedKey extends addPrefix<infer TKey, TPrefix>
? TKey
: '';
type prefixedValue<TObject extends object, TPrefixedKey extends string, TPrefix extends string> = TObject extends {[K in removePrefix<TPrefixedKey, TPrefix>]: infer TValue}
? TValue
: never;
type addPrefixToObject<TObject extends object, TPrefix extends string> = {
[K in addPrefix<keyof TObject, TPrefix>]: prefixedValue<TObject, K, TPrefix>
}
addPrefix
takes in an existing TKey
and adds a TPrefix
to it, if TKey
extends the type string. If it does not extend the type string
, never
is returned instead as a type.removePrefix
takes in a TPrefixedKey
and a TPrefix
and removes TPrefix
from the key by using infer
to retrieve the original key that was used to create the TPrefixedKey
.prefixedValue
takes in a TObject
which is not prefixed. Then it infers TValue
from the TPrefixedKey
after the prefix was removed with removePrefix
. If this succeeds TValue
is returned. Otherwise, an empty string is returned, which is still a valid object signature.addPrefixToObject
puts all of the above together. It maps over all of the keys which are currently inside TObject
and prefixes them. The value is retrieved by using prefixedValue
.If you put this into action it seems to work out quite fine:
const myConfig: OptionsConfig = {};
// Works:
myConfig.attr1 = {name: 'name'};
myConfig.attr2 = {"@parts": 1};
myConfig.attr3 = {"@parts": 1, name: 'name'};
// Error: Type '{ parts: number; }' is not assignable to type 'OptionsA | addPrefixToObject<OptionsB, "@">'.
myConfig.attr4 = {"parts": 1};
// Prints as:
// type prefixedOptionB = {
// "@parts": number;
// }
type PrefixedOptionB = addPrefixToObject<OptionsB, '@'>;
And autocompletion is also working accordingly:
All of this can probably be optimized further, so if you have any suggestions please leave a comment. Have a nice day 👋
Full example on TypeScript playground is here.
Typescript 4.1 gave us key remapping, so you can do something like this:
type addPrefixToObject<T, P extends string> = {
[K in keyof T as K extends string ? `${P}${K}` : never]: T[K]
}
Then, your OptionsConfig
becomes
export interface OptionsConfig {
[key: string]: OptionsA | addPrefixToObject<OptionsB, '@'>;
}
Example available in Typescript playground.
Based on the other answers I have an object colors
for Stitches but for each color used as a prop in a component I do the following.
const colors = {
white: '#ffffff',
black: '#000000',
transparent: 'transparent',
text: '$black',
background: '$white',
indigo: '#0757D6',
purple: '#7928ca',
success: '#17c964',
warning: '#f5a623',
yellow2: '#F2C94C',
error: '#f21361',
primary: '#518AE3',
secondary: '$purple',
icon: '#BBBBBB',
primaryOpacity: '#DEEBFF',
secondaryOpacity: '#DBC8F0',
errorOpacity: '#FFC3D8',
subText: '#707070',
gray1: '#333333',
bgCard: '#F9F9FA',
bgText: 'rgba(234, 234, 234, 0.7)',
};
To get ColorType
automatic try this:
type addPrefixToObject<TObject extends object, TPrefix extends string> = `${TPrefix}${keyof TObject}`
export type ColorType = addPrefixToObject<typeof colors, '$'>;
This returns a type that will be "$black" | "$white" | "$text"
(and so on). I.e. it returns each color key with the prefix $
.
One quick answer for enforcing prefix using types (Starting in TS4.1)-
`type strWithPre = pre-${string}`
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With