Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Define prefix for object keys using types in TypeScript

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 @wrongcan only implement the OptionsB interface, because it has the @ prefix.

Alternatively, is there another approach to achieve a similar kind of restriction.

like image 559
Reactgular Avatar asked Aug 15 '19 13:08

Reactgular


4 Answers

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: screenshot of autocompletion


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.

like image 199
NiklasPor Avatar answered Oct 18 '22 02:10

NiklasPor


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.

like image 44
azizj Avatar answered Oct 18 '22 00:10

azizj


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

like image 21
Kevin Rivas Avatar answered Oct 18 '22 00:10

Kevin Rivas


One quick answer for enforcing prefix using types (Starting in TS4.1)-

`type strWithPre = pre-${string}`
like image 1
Romi Erez Avatar answered Oct 18 '22 01:10

Romi Erez