Skip to content

Instantly share code, notes, and snippets.

@kaave
Last active September 25, 2021 16:06
Show Gist options
  • Save kaave/90be8a8fe95fd72eb5c72ead64b7d1ba to your computer and use it in GitHub Desktop.
Save kaave/90be8a8fe95fd72eb5c72ead64b7d1ba to your computer and use it in GitHub Desktop.
My Branded Type Pattern
import * as Hex from './hex';
const red = Hex.create('#f00');
const nonBrandedRed = '#f00';
function stdoutOnlyHex(hex: Hex) {
console.log(hex);
}
stdoutOnlyHex(red);
stdoutOnlyHex(nonBrandedRed); // Error
import { GraphQLScalarType, Kind } from 'graphql';
import type { GraphQLScalarTypeConfig } from 'graphql';
import { hex } from '.';
import type { Hex } from '.';
function generateHex() {
/* cspell: disable-next-line */
return `#${Math.floor(Math.random() * 255).toString(16)}${Math.floor(Math.random() * 255).toString(16)}${Math.floor(
Math.random() * 255,
).toString(16)}`;
}
const hexCore: GraphQLScalarTypeConfig<string, Hex> = {
name: 'Hex',
// value sent to the client
serialize: (v: Hex) => v,
// value from the client
parseValue: v => hex(v),
// ast value is always in string format
parseLiteral: ast => (ast.kind === Kind.STRING ? hex(ast.value) : null),
};
export const GraphQLUuid = new GraphQLScalarType(hexCore);
export const MockGraphQLUuid = () => new GraphQLScalarType({ ...hexCore, name: generateHex() });
import * as Hex from '.';
describe('Hex', () => {
describe('is', () => {
it('0-9a-zA-Z は Valid', () => {
expect(Hex.is('#000')).toBe(true);
expect(Hex.is('#0f0')).toBe(true);
expect(Hex.is('#9f0')).toBe(true);
expect(Hex.is('#FFF')).toBe(true);
});
it('全角は Invalid', () => {
expect(Hex.is('#000')).toBe(false);
expect(Hex.is('#0f0')).toBe(false);
expect(Hex.is('#9f0')).toBe(false);
expect(Hex.is('#FFF')).toBe(false);
expect(Hex.is('#FFF')).toBe(false);
});
it('3, 6桁以外は Invalid', () => {
expect(Hex.is('#00')).toBe(false);
expect(Hex.is('#0000')).toBe(false);
expect(Hex.is('#00000')).toBe(false);
expect(Hex.is('#0000000')).toBe(false);
expect(Hex.is('#00000000')).toBe(false);
expect(Hex.is('#000000000')).toBe(false);
});
it('範囲外 は Invalid', () => {
expect(Hex.is('#g00')).toBe(false);
expect(Hex.is('#00G')).toBe(false);
});
});
});
// validation は is という命名で統一
export function is(possibleHex: string): possibleHex is Hex {
return /^#([\da-fA-F]{2}){3}$/.test(possibleHex) || /^#([\da-fA-F]{1}){3}$/.test(possibleHex);
}
// factory は create という命名で統一
export function create(from: string): Hex {
if (is(from)) {
return from as Hex; // ここの cast は許容するため、場合によっては Linter を disabled する
}
throw new TypeError(`Invalid Hex format: [${from}]`);
}
// 型そのものは global にしたいので ambience
// 衝突に重々注意する
// `__${typename}Brand` という key を readonly の unique symbol で使用する
// https://basarat.gitbook.io/typescript/main-1/nominaltyping
declare type Hex = `#{string}` & { readonly __hexBrand: unique symbol };
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment