Skip to content

Instantly share code, notes, and snippets.

@renoirb
Created May 25, 2020 13:19
Show Gist options
  • Save renoirb/d30e7edbe32d9c6b9c2006b13db7a016 to your computer and use it in GitHub Desktop.
Save renoirb/d30e7edbe32d9c6b9c2006b13db7a016 to your computer and use it in GitHub Desktop.
TypeScript type Discriminator factory
export enum EmployeeRole {
DEVELOPER = 100,
PRODUCT_MANAGER = 200,
PRODUCT_DESIGNER = 300,
}
/**
* This is equivalent to:
*
* type EmployeeRoleType = 'SOFTWARE_DEVELOPER' | 'SOFTWARE_DEVELOPER_COOP';
*/
export type EmployeeRoleKeys = keyof typeof EmployeeRole;
/**
* Should we want to make sure we have a label for EVERY EmployeeRole
*
* Bookmarks:
* - https://stackoverflow.com/a/51832623
*/
export type EmployeeRoleMustHave<T> = {
[K in EmployeeRoleKeys]: Extract<K, EmployeeRoleKeys> extends never ? never : T;
};
/**
* Translation label hash-map for Employee Roles.
* So we can display text instead of a number.
*/
const ROLES: EmployeeRoleMustHave<string> = {
DEVELOPER: 'Software Developer',
PRODUCT_MANAGER: 'Product Manager',
PRODUCT_DESIGNER: 'Product Designer',
};
/**
* Build up all possible role labels.
* This is here to help be re-used elsewhere, from both Express and Angular.
* That's also why we're enforcing above the ROLES map to make sure we don't miss.
*/
const ownSubset = Object.entries(EmployeeRole).filter(i => 1 in i && typeof i[1] === 'number') as [string, number][];
const rolesLabels = ownSubset.map(i => ([i[1], ROLES[i[0]]])) as [number, string][];
// Return only a copy when called. Just to be sure we do not mutate.
export const EmployeeRolesLabelMap = (() => new Map<number, string>(rolesLabels))();
export interface Employee {
role: EmployeeRole;
}
/**
* Runtime Typing utility.
*
* Take an HashMap object, extract keys,
* and possibly make it a type
*/
export const convertRecordToDiscriminator = <T>(
hashMap: T,
): ReadonlyArray<keyof T> => {
const k = Object.keys(hashMap) as (keyof T)[];
return Object.freeze(k);
};
@renoirb
Copy link
Author

renoirb commented Jan 28, 2021

This Gist is one more use-case expanded in Renoir's references to books and chapters

@renoirb
Copy link
Author

renoirb commented Mar 3, 2021

There has been other attempts but were getting bigger and bigger

export enum UserType {
  fooName = 'fooName',
}

export const USER_TYPE_KEYS = Object.freeze<keyof typeof UserType>(
  Object.keys(UserType)
    .map<keyof typeof UserType>(key => UserType[key])
    .filter(value => typeof value === 'string'),
)

export const USER_TYPE_ITEM_KEYS = Object.freeze<keyof UserCredential>(
  Object.keys(UserType[USER_TYPE_KEYS[0]])
    .map<keyof UserCredential>(key => UserType[USER_TYPE_KEYS[0]][key])
    .filter(value => typeof value === 'string'),
)

export const assertsIsUserCredientialMap: (maybe: unknown) => asserts maybe is UserCredientialMap = maybe => {
  try {
    const maybeUserTypeKeys = Object.keys(maybe)
    assert.equal(maybeUserTypeKeys.length > 0, true)
    // Has the same keys
    assert.equal(maybeUserTypeKeys, USER_TYPE_KEYS)
    // Exactly the same count of keys
    assert.equal(maybeUserTypeKeys.length, USER_TYPE_KEYS.length)
    for (const [, userTypeItem] of Object.entries(maybe)) {
      const userTypeItemKeys = Object.keys(userTypeItem)
      assert.equal(userTypeItemKeys, USER_TYPE_ITEM_KEYS)
      assert.equal(userTypeItemKeys.length, USER_TYPE_ITEM_KEYS.length)
    }
  } catch (e) {
    throw new TypeError(`Is not an UserCredentialMap ${e}`)
  }
}

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment