Skip to content

Instantly share code, notes, and snippets.

@theScottyJam
Created January 31, 2023 02:00
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save theScottyJam/084c631adc5da823e4672f0d97287cc1 to your computer and use it in GitHub Desktop.
Save theScottyJam/084c631adc5da823e4672f0d97287cc1 to your computer and use it in GitHub Desktop.
Example of using the return-your-exceptions pattern
import type { User } from './userManager';
export const users: Map<string, User> = new Map(Object.entries({
'1': {
id: '1',
username: 'admin',
permissions: ['VIEW', 'UPDATE'],
},
'2': {
id: '2',
username: 'bob',
permissions: ['VIEW'],
age: 7,
},
'3': {
id: '3',
username: 'sally',
permissions: [],
age: 9,
},
}));
import * as userManager from './userManager';
type Permission = userManager.Permission;
type User = userManager.User;
type GetUsernameOpts<Fallback> = { userIdDoingRequest: string, fallback: Fallback };
type GetUsernameExits<Fallback> = { type: 'ok', value: string | Fallback } | { type: 'unauthorized' }
function assert(condition: boolean, message = 'This program unexpectedly ended up in a bad state.'): asserts condition {
if (!condition) {
throw new Error(message);
}
}
function getUsername<Fallback>(userId: string, { userIdDoingRequest, fallback }: GetUsernameOpts<Fallback>): GetUsernameExits<Fallback> {
const maybeUsername = userManager.getProperty(userId, 'username', { userIdDoingRequest });
if (maybeUsername.type === 'notFound') {
return { type: 'ok', value: fallback };
} else if (maybeUsername.type === 'unauthorized') {
return maybeUsername;
} else if (maybeUsername.type === 'missingProp') {
// getProperty() returned missingProp as an exception that we could choose to handle.
// But, since the 'username' field being passed in is hard-coded and usernames are a required field,
// we know this exception should never happen, so it it does, we throw it as an error here, indicating that
// there's a bug in the code.
throw new Error(`Missing Prop: ${maybeUsername.message}`);
}
return { type: 'ok', value: maybeUsername.value };
}
type IncrementAgeOpts = { userIdDoingRequest: string, noopIfUsernotFound?: boolean }
type IncrementAgeExits = { type: 'ok' } | { type: 'unauthorized' };
function incrementAge(userId: string, { userIdDoingRequest, noopIfUsernotFound = false }: IncrementAgeOpts): IncrementAgeExits {
const maybeAge = userManager.getProperty(userId, 'age', { userIdDoingRequest });
if (maybeAge.type === 'notFound') {
if (noopIfUsernotFound) return { type: 'ok' };
throw new Error('User not found');
} else if (maybeAge.type === 'unauthorized') {
return maybeAge;
} else if (maybeAge.type === 'missingProp') {
// Escalating this exception into an error, because, we know that the way we're calling getProperty(), this should never happen,
// so if it does, we throw an error.
throw new Error(`Missing Prop: ${maybeAge.message}`);
}
assert(maybeAge.value !== undefined, 'Can only increment the age of a user who has an age property');
const maybeSuccess = userManager.setProperty(userId, 'age', maybeAge.value + 1, { userIdDoingRequest });
if (maybeSuccess.type === 'unauthorized') {
throw new Error(
`User with id ${userId} was not authorized to increment another's age. ` +
'Please make sure they have the appropriate permissions before calling this function.'
);
}
assert(maybeSuccess.type === 'ok');
return { type: 'ok' };
}
type GivePermissionToUsersOpts = { userIdDoingRequest: string };
type givePermissionToUsers = { type: 'ok' } | { type: 'unauthorized' } | { type: 'notFound', message: string };
function givePermissionToUsers(permission: Permission, userIds: string[], { userIdDoingRequest }: GivePermissionToUsersOpts): givePermissionToUsers {
const allowed = (
userManager.doesUserHasPermission(userIdDoingRequest, 'VIEW') &&
userManager.doesUserHasPermission(userIdDoingRequest, 'UPDATE')
);
if (!allowed) {
return { type: 'unauthorized' };
}
const users: User[] = [];
for (const userId of userIds) {
const maybeUser = userManager.getUser(userId, { userIdDoingRequest })
if (maybeUser.type === 'notFound') {
return { type: 'notFound', message: `User with ID ${userId} not found.` };
} else if (maybeUser.type === 'unauthorized') {
throw new Error(`Unexpected failure when fetching users. Received unauthorized response.`);
}
// Note that if a new exception return type is ever added to the getUser() response, TypeScript will flag this piece
// of code, and notify us that it needs to be updated to handle it. You can try it to see how it works.
users.push(maybeUser.value);
}
// Only start doing modifications after we've checked that all users exist.
for (const user of users) {
if (!user.permissions.includes(permission)) {
const newPermissions = [...user.permissions, permission];
const setPropertyResponse = userManager.setProperty(user.id, 'permissions', newPermissions, { userIdDoingRequest });
assert(setPropertyResponse.type === 'ok');
}
}
return { type: 'ok' };
}
function main() {
const maybeUsername = getUsername('2', { userIdDoingRequest: '3', fallback: null });
if (maybeUsername.type !== 'unauthorized') throw new Error('UNREACHABLE');
console.log('User with id 3 does not have permissions to view other users');
givePermissionToUsers('VIEW', ['3'], { userIdDoingRequest: '1' });
const usernameResult = getUsername('2', { userIdDoingRequest: '3', fallback: null });
assert(usernameResult.type === 'ok');
console.log('Username of id 2 is: ' + usernameResult.value);
const incrementAgeResult1 = incrementAge('5', { userIdDoingRequest: '1', noopIfUsernotFound: true });
assert(incrementAgeResult1.type === 'ok');
const ageResult1 = userManager.getProperty('3', 'age', { userIdDoingRequest: '1' });
if (ageResult1.type !== 'ok') throw new Error(`Unexpected exit type: ${ageResult1.type}`);
console.log('age before increment: ' + ageResult1.value);
const incrementAgeResult2 = incrementAge('3', { userIdDoingRequest: '1' });
assert(incrementAgeResult2.type === 'ok');
const ageResult2 = userManager.getProperty('3', 'age', { userIdDoingRequest: '1' });
assert(ageResult2.type === 'ok');
console.log('age after increment: ' + ageResult2.value);
/* Expected output
User with id 3 does not have permissions to view other users
Username of id 2 is: bob
age before increment: 9
age after increment: 10
*/
}
main();
import { users } from './exampleData';
export type Permission = 'VIEW' | 'UPDATE';
export interface User {
id: string
username: string
permissions: Permission[]
age?: number
}
export const PERMISSIONS = {
view: 'VIEW',
update: 'UPDATE',
};
type GetUserOpts = { userIdDoingRequest: string };
type GetUserExits = { type: 'ok', value: User } | { type: 'notFound' } | { type: 'unauthorized' };
export function getUser(userId: string, { userIdDoingRequest }: GetUserOpts): GetUserExits {
if (!doesUserHasPermission(userIdDoingRequest, 'VIEW')) {
return { type: 'unauthorized' };
}
const user = users.get(userId);
if (!user) {
return { type: 'notFound' };
}
return { type: 'ok', value: user };
}
type GetPropertyOpts = { userIdDoingRequest: string };
type GetPropertyExits<Key extends keyof User> = { type: 'ok', value: User[Key] }
| { type: 'notFound' }
| { type: 'unauthorized' }
| { type: 'missingProp', message: string };
export function getProperty<Key extends keyof User>(userId: string, propName: Key, { userIdDoingRequest }: GetPropertyOpts): GetPropertyExits<Key> {
const maybeUser = getUser(userId, { userIdDoingRequest });
if (maybeUser.type === 'notFound' || maybeUser.type === 'unauthorized') {
return maybeUser;
}
const user = maybeUser.value;
if (Object.hasOwn(user, propName)) {
return { type: 'ok', value: user[propName] };
} else {
return {
type: 'missingProp',
message: `The property "${propName}" does not exist on the provided user.`,
};
}
}
type SetPropertyOpts = { userIdDoingRequest: string };
type SetPropertyExits = { type: 'ok' }
| { type: 'notFound' }
| { type: 'unauthorized' }
| { type: 'missingProp' };
export function setProperty<Key extends keyof User>(userId: string, propName: Key, value: User[Key], { userIdDoingRequest }: SetPropertyOpts): SetPropertyExits {
if (!doesUserHasPermission(userIdDoingRequest, 'UPDATE')) {
return { type: 'unauthorized' };
}
const maybeUser = getUser(userId, { userIdDoingRequest });
if (maybeUser.type === 'notFound' || maybeUser.type === 'unauthorized') {
return maybeUser;
}
const user = maybeUser.value;
if (!Object.prototype.hasOwnProperty.call(user, propName)) {
return { type: 'missingProp' };
}
user[propName] = value;
return { type: 'ok' };
}
export function doesUserHasPermission(userId: string, permission: Permission): boolean {
const user = users.get(userId);
if (!user) {
throw new Error('The ID of the user doing the request was not found.');
}
return user.permissions.includes(permission);
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment