Hegel: Objects to unions converter

Created on 29 Apr 2020  路  6Comments  路  Source: JSMonk/hegel

Let's say we have a dictionary:

const FEATURES = {
  SORTING: 'Sorting',
  FILTERING: 'Filtering',
  REORDERING: 'Reordering',
};

It can be used like this:

const features = [];

// or it can be a method of a class like "addFeature"
features.push(FEATURES.FILTERING);

I want to say to my features array that it can accept values of the FEATURES object only. But in order to avoid creating a separate union type and support (possible) changes there and in FEATURES object, I'd like to collect values from the object and keep them as a union type. Thus if it is necessary to add/remove value, it can be done in only one place - in the FEATURES object.

To achieve this goal, in TypeScript it is necessary to define such monstrous type:

const FEATURES = {
  SORTING: 'Sorting',
  FILTERING: 'Filtering',
  REORDERING: 'Reordering',
// 1. const assertions
} as const;

// 2. if we have a lot of such dictionaries, we have to repeat this stuff every time
type Feature = (typeof FEATURES)[keyof typeof FEATURES];

// if we want to get union type of keys
// type FeatureKeys = keyof typeof FEATURES;

const features: Feature[] = [];

// OK
features.push(FEATURES.FILTERING);

/*
  Should throw a type error
  Argument of type '"foo"' is not assignable to parameter of type
  '"Sorting" | "Filtering" | "Reordering"'.
*/
features.push('foo')

Are there any thoughts of optimizing this type generation to avoid repeating such boilerplate in Hegel? Like a help function or something.

And I'd like to know if it is a common thing to work with dictionaries this way or not. Because I'm new to strong type checking and maybe I think the wrong way here...

Thanks in advance.

question

All 6 comments

  • Object.freeze function will do what as const does
  • $TypeOf magic type will do what typeof does
  • $Keys magic type does what keyof does but if you want values $Values magic type will do that
const FEATURES = Object.freeze({
  SORTING: 'Sorting',
  FILTERING: 'Filtering',
  REORDERING: 'Reordering'
})

type Feature = $Values<$TypeOf<FEATURES>>

const features: Array<Feature> = []

void features.push('Filtering') // ok
void features.push(FEATURES.FILTERING) // ok

void features.push('foo') // error as expected

try

only down side would be that FEATURES will be frozen on runtime as well (this may or may not be desired behavior)

if you don't like to freeze object on runtime you can create helper immutableId function that will refine type but don't change value for runtime

const immutableId = <T>(v: T): $Immutable<T> => v

const FEATURES = immutableId({
  SORTING: 'Sorting',
  FILTERING: 'Filtering',
  REORDERING: 'Reordering'
})

@thecotne, thanks for your examples. It makes the code a little bit easier, but the problem actually is in Feature type definition repetition. I tried to apply immutableId approach to Feature, but failed of course :)

I tried to get something like this:

// without output type because have no ideas how to describe it
const makeUnion = (obj: object) => $Values<$TypeOf<obj>>;

type Feature = makeUnion(FEATURES);

but it seems that type definition doesn't work this way :)

this works perfectly fine you don't need to repeat anything ...

const immutableId = <T>(v: T): $Immutable<T> => v

const FEATURES = immutableId({
  SORTING: 'Sorting',
  FILTERING: 'Filtering',
  REORDERING: 'Reordering'
})

type Feature = $Values<$TypeOf<FEATURES>>

const features: Array<Feature> = []

void features.push('Filtering') // ok
void features.push(FEATURES.FILTERING) // ok

void features.push('foo') // error as expected

try

you just need to use either Object.freeze or immutableId to achieve same result as as const in typescript.

@thecotne, thanks for your reply!

Actually I found out the answer to my initial question (with your help and my own research). Here is code snippets with the solutions:

For Hegel:

const FEATURES = Object.freeze({
  SORTING: 'Sorting',
  FILTERING: 'Filtering',
  REORDERING: 'Reordering',
});

// "Array" should be outside
// type Values<T> = Array<$Values<T>>;

// 2. Define this type
// Array<'Filtering' | 'Reordering' | 'Sorting'>
const features: Array<$Values<$TypeOf<FEATURES>>> = [];

// OK
void features.push(FEATURES.FILTERING);

/*
  Throws a type error
  Argument of type '"foo"' is not assignable to parameter of type
  '"Sorting" | "Filtering" | "Reordering"'.
*/
void features.push('foo');

For TypeScript:

// 1. Freeze the object
const FEATURES = {
  SORTING: 'Sorting',
  FILTERING: 'Filtering',
  REORDERING: 'Reordering',
} as const;

type Values<T> = Array<T[keyof T]>;

// 2. Define this type
// ("Sorting" | "Filtering" | "Reordering")[]
const features: Values<typeof FEATURES> = [];

// OK
void features.push(FEATURES.FILTERING);

/*
  Throws a type error
  Argument of type '"foo"' is not assignable to parameter of type
  '"Sorting" | "Filtering" | "Reordering"'.
*/
void features.push('foo');

The answer I tried to find out is Values type implementation to reduce code repetitiveness.
For any reason Array<T> can't be moved to Values type in Hegel unlike TypeScript though.
If it is a known issue, I suppose this one can be closed.

@vasilii-kovalev i have reported Values type issue separately https://github.com/JSMonk/hegel/issues/269

Was this page helpful?
0 / 5 - 0 ratings

Related issues

thecotne picture thecotne  路  5Comments

trusktr picture trusktr  路  5Comments

artalar picture artalar  路  4Comments

jarble picture jarble  路  4Comments

thecotne picture thecotne  路  4Comments