Repository: modfy/nominal Branch: master Commit: 00faaf156930 Files: 14 Total size: 17.4 KB Directory structure: gitextract_m2ssrx3h/ ├── .gitignore ├── .vscode/ │ └── settings.json ├── Readme.md ├── examples/ │ ├── basic.ts │ ├── composing.ts │ ├── proportionalityConstant.ts │ ├── safeRecord.ts │ └── sort.ts ├── package.json ├── rome.json ├── src/ │ ├── index.ts │ ├── proportionalityConstant.ts │ └── standardLib.ts └── tsconfig.json ================================================ FILE CONTENTS ================================================ ================================================ FILE: .gitignore ================================================ node_modules ================================================ FILE: .vscode/settings.json ================================================ { "[typescriptreact]": { "editor.defaultFormatter": "rome.rome" }, "[typescript]": { "editor.defaultFormatter": "rome.rome" }, "[json]": { "editor.defaultFormatter": "esbenp.prettier-vscode" } } ================================================ FILE: Readme.md ================================================ # Nominal The right way to do types in typescript. ## Installation ```bash npm install nominal-types yarn install nominal-types pnpm install nominal-types ``` # Usage ## Basic The most immediate benefit of nominal types is preventing confusion between two types. In regular Typescript you run into this problem: ```ts type Minutes = number type Seconds = number const minutesToSeconds = (minutes: Minutes) => minutes * 60 const seconds: Seconds = 420 // uh-oh, we can use Minutes and Seconds interchangeably minutesToSeconds(seconds) ``` Nominal types solve this problem ```ts import { Nominal, nominal } from 'nominal-types'; type Minutes = Nominal<'Minutes', number>; type Seconds = Nominal<'Seconds', number>; const minutesToSeconds = (minutes: Minutes) => minutes * 60 // You can directly type cast or use nominal.make const seconds = nominal.make(420) const minutes = 1337 as Minutes // doesn't work, yay type safety minutesToSeconds(seconds) // does work! minutesToSeconds(minutes) ``` ## Another example You can use nominal types to give your code even better type-safety guarantees. This goes **beyond just type-safety**, it's a performance optimization: once you know the array is sorted, you never have to sort it again. This is enforcing that at a type level. ```typescript type SortedArray = Nominal<'sortedArray', Array> const sort = (arr: Array): SortedArray => arr.sort() const binarySearch = ( sorted: SortedArray, search: T ): number | undefined => { /* ... */ } const regularArray = [1, 7, 2, 3, 6, 9, 10, 4, 5] // won't work binarySearch(regularArray, 2) // will work binarySearch(sort(regularArray), 3) ``` This is also known as [Refinement types](https://en.wikipedia.org/wiki/Refinement_type) ## Composing types We can actually make this a bit crazier, we can compose nominal types ```ts type SortedArray = Nominal<'sortedArray', Array> const sort = (arr: Array): SortedArray => arr.sort() as SortedArray const nonEmpty = (arr:Array):NonEmptyArray => arr.filter(Boolean) as NonEmptyArray type NonEmptyArray> = Nominal<'nonEmptyArray', T>; type NonEmptySorted = NonEmptyArray>; const binarySearch = (sorted: NonEmptySorted): T => { let foo = sorted[0] return foo } // won't work binarySearch(regularArray) // still won't work binarySearch(sort(regularArray)) binarySearch(nonEmpty(sort(regularArray))) ``` ## Examples More examples in [examples folder](./examples), you can also see them typed on replit. | Example | Link | |-------------|-----------------------------------------------------------| | basic | https://replit.com/@CryogenicPlanet/Nominal#basic.ts | | sorting | https://replit.com/@CryogenicPlanet/Nominal#sort.ts | | composing | https://replit.com/@CryogenicPlanet/Nominal#composing.ts | | safeRecords | https://replit.com/@CryogenicPlanet/Nominal#safeRecord.ts | ## Credits You can read more about this https://zackoverflow.dev/writing/nominal-and-refinement-types-typescript Inspiration from [Ghosts of Departed Proofs (Functional Pearl)](https://kataskeue.com/gdp.pdf) ================================================ FILE: examples/basic.ts ================================================ // After Nominal import { Nominal } from '../src'; import { Equal, Expect } from '@type-challenges/utils'; type Minutes = Nominal<'Minutes', number>; type Seconds = Nominal<'Seconds', number>; const minutesToSeconds = (minutes: Minutes): Seconds => (minutes * 60) as Seconds; // You can directly type cast or use nominal.make const seconds = 420 as Seconds; const minutes = 1337 as Minutes; // @ts-expect-error - doesn't work, yay type safety minutesToSeconds(seconds); // does work! minutesToSeconds(minutes); // @ts-expect-error - won't work, yay! minutesToSeconds(minutesToSeconds(minutes)); ================================================ FILE: examples/composing.ts ================================================ import { Nominal } from '../src'; type SortedArray = Nominal<'sortedArray', T[]>; const sort = (arr: T[]): SortedArray => arr.sort() as SortedArray; const nonEmpty = (arr: T): NonEmptyArray => { if (arr.length === 0) { throw new Error('Array is empty'); } return arr as NonEmptyArray; }; type NonEmptyArray = Nominal<'nonEmptyArray', T>; type NonEmptySorted = NonEmptyArray>; // Not implemented const binarySearch = (sorted: NonEmptySorted): T => { let foo = sorted[0]; // @ts-ignore return foo; }; const regularArray = [1, 2, 3]; // @ts-expect-error - won't work binarySearch(regularArray); // @ts-expect-error - still won't work binarySearch(sort(regularArray)); // will work binarySearch(nonEmpty(sort(regularArray))); ================================================ FILE: examples/proportionalityConstant.ts ================================================ import { Equal, Expect } from '@type-challenges/utils'; import { divideWithK, multiplyWithK, Nominal, ProportionalityConstant, } from '../src'; export type Pixel = Nominal<'Pixel', number>; export type Seconds = Nominal<'Seconds', number>; export type PixelPerSecond = ProportionalityConstant; const PIXEL_PER_SECOND: PixelPerSecond = 1 as PixelPerSecond; const pixels = 100 as Pixel; const seconds = 1 as Seconds; const a = multiplyWithK(PIXEL_PER_SECOND, seconds); type A = typeof a; // @ts-expect-error - should not be able to divide with seconds const _b = divideWithK(PIXEL_PER_SECOND, seconds); const c = divideWithK(PIXEL_PER_SECOND, pixels); type C = typeof c; type _cases = [Expect>, Expect>]; ================================================ FILE: examples/safeRecord.ts ================================================ import { Nominal } from '../src'; type SafeRecord = Nominal< 'safeRecordSymbol', Record >; const newRecord = ( record: Record, value: Value, ): SafeRecord => ({ [record]: value }) as SafeRecord; const add = ( record: SafeRecord, key: R2, value: V, ): SafeRecord => { Object.assign(record, { [key]: value }); return record as SafeRecord; }; const safeRecord = newRecord('a', 'b'); // @ts-expect-error - Won't work const x = safeRecord['random']; // any const lmao = add(safeRecord, 'lmao', 'nice'); lmao.a; // string lmao.lmao; // string // @ts-expect-error - Won't work lmao['random']; // any - Untyped ================================================ FILE: examples/sort.ts ================================================ import { Nominal } from '../src'; type SortedArray = Nominal<'sortedArray', T[]>; const sort = (arr: T[]): SortedArray => arr.sort() as SortedArray; const binarySearch = ( sorted: SortedArray, search: T, ): number | undefined => { if (sorted.length !== 0) { const midPoint = sorted.length / 2; if (sorted[midPoint] === search) { return midPoint; } // Bang: Midpoint will exist if (search > sorted[midPoint]!) { return binarySearch(sorted.slice(midPoint) as SortedArray, search); } else { return binarySearch(sorted.slice(0, midPoint) as SortedArray, search); } } else { return undefined; } }; const regularArray = [1, 7, 2, 3, 6, 9, 10, 4, 5]; // @ts-expect-error won't work binarySearch(regularArray, 2); // will work binarySearch(sort(regularArray), 2); ================================================ FILE: package.json ================================================ { "name": "nominal-types", "version": "0.2.0", "description": "Nominal types for better typesafety", "main": "src/index.ts", "types": "src/index.ts", "keywords": [ "nominal", "typesafety", "custom", "ids" ], "author": "modfy", "license": "MIT", "files": [ "src/" ], "devDependencies": { "@type-challenges/utils": "^0.1.1", "rome": "^10.0.1", "typescript": "^5.0.2" } } ================================================ FILE: rome.json ================================================ { "linter": { "enabled": true, "rules": { "recommended": true } }, "formatter": { "enabled": true, "indentStyle": "space" }, "javascript": { "formatter": { "quoteStyle": "single" } } } ================================================ FILE: src/index.ts ================================================ const M = Symbol(); export type Nominal = Type & { readonly [M]: [Name]; }; export * from './standardLib'; export * from './proportionalityConstant'; ================================================ FILE: src/proportionalityConstant.ts ================================================ // export type xPERy = /** * ProportionalityConstant is a type that represents a constant that is used to convert from one type to another * * Y ∝ X -> Y = λX > =kX * * This type is the type of the constant k in the above equation, it can used with the multiplyWithK and divideWithK functions to convert between the two types * * @example type PixelPerSecond = ProportionalityConstant * * const pixels: Pixels = multiplyWithK(PIXEL_PER_SECOND, 1 as Seconds) * * const seconds: Seconds = divideWithK(PIXEL_PER_SECOND, 1 as Pixels) * */ export type ProportionalityConstant< Y extends number, X extends number, > = number & { readonly __X: X; readonly __Y: Y; }; type multiplyWithKReturn = K extends ProportionalityConstant< infer Y, infer _X > ? Y : never; type multiplyWithKInferX = K extends ProportionalityConstant< infer _Y, infer X > ? X : never; type divideWithKReturn = K extends ProportionalityConstant ? X : never; type divideWithKInferY = K extends ProportionalityConstant ? Y : never; /** * A function to get the converted typed after multiplying with proportionality constant * * @param k - ProportionalityConstant -> Y ∝ X -> Y = λX -> Y = kX * @param x - Value of type X * @returns y - Value of type Y */ export const multiplyWithK = ( k: K, x: multiplyWithKInferX, ): multiplyWithKReturn => (k * x) as multiplyWithKReturn; /** * A function to get the converted typed after dividing with proportionality constant * * @param k - ProportionalityConstant -> Y ∝ X -> X = Y/k * @param y - Value of type Y * @returns x - Value of type X */ export const divideWithK = >( k: K, y: divideWithKInferY, ): divideWithKReturn => (y / k) as divideWithKReturn; ================================================ FILE: src/standardLib.ts ================================================ import { Nominal } from './index'; /*** * This is a replacement for Object.keys which actually types the keys correctly * * In javascript Object.keys makes the keys strings, so this can only be used with the record is using string keys */ export const keys = (obj: Record): T[] => Object.keys(obj) as T[]; /** * Add values of the same type, a way to add values of some nominal of number can get the sum of the values typed correctly */ export const plus = (...arg: T[]): T => { const sum = arg.reduce((acc, cur) => acc + cur, 0); return sum as T; }; /** * Subtract values of the same type, a way to subtract values of some nominal of number can get the difference of the values typed correctly */ export const minus = (a: T, b: T): T => { return (a - b) as T; }; /** * Decorate number as negative value */ export type Negative = Nominal<'Negative', T>; /** * Create a number decorated as negative, will throw an error if the number is not negative */ export const Negative = (value: T): Negative => { if (value > 0) { throw new Error('Value must be negative'); } return value as Negative; }; /** * Decorate number as zero */ export type Zero = Nominal<'Zero', T>; /** * Decorate number as negative or zero */ export type NegativeOrZero = Negative | Zero; /** * Decorate number as negative or zero * * Will throw an error if the number is positive */ export const NegativeOrZero = ( value: T, ): NegativeOrZero => { if (value > 0) { throw new Error('Value must be negative or zero'); } return value as NegativeOrZero; }; ================================================ FILE: tsconfig.json ================================================ { "compilerOptions": { /* Visit https://aka.ms/tsconfig.json to read more about this file */ /* Basic Options */ // "incremental": true, /* Enable incremental compilation */ "target": "ESNEXT", /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019', 'ES2020', 'ES2021', or 'ESNEXT'. */ "module": "ESNext", /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', 'es2020', or 'ESNext'. */ // "lib": [], /* Specify library files to be included in the compilation. */ // "allowJs": true, /* Allow javascript files to be compiled. */ // "checkJs": true, /* Report errors in .js files. */ // "jsx": "preserve", /* Specify JSX code generation: 'preserve', 'react-native', 'react', 'react-jsx' or 'react-jsxdev'. */ // "declaration": true, /* Generates corresponding '.d.ts' file. */ // "declarationMap": true, /* Generates a sourcemap for each corresponding '.d.ts' file. */ // "sourceMap": true, /* Generates corresponding '.map' file. */ // "outFile": "./", /* Concatenate and emit output to single file. */ // "outDir": "./", /* Redirect output structure to the directory. */ // "rootDir": "./", /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */ // "composite": true, /* Enable project compilation */ // "tsBuildInfoFile": "./", /* Specify file to store incremental compilation information */ // "removeComments": true, /* Do not emit comments to output. */ "noEmit": true, /* Do not emit outputs. */ // "importHelpers": true, /* Import emit helpers from 'tslib'. */ // "downlevelIteration": true, /* Provide full support for iterables in 'for-of', spread, and destructuring when targeting 'ES5' or 'ES3'. */ // "isolatedModules": true, /* Transpile each file as a separate module (similar to 'ts.transpileModule'). */ /* Strict Type-Checking Options */ "strict": true, /* Enable all strict type-checking options. */ // "noImplicitAny": true, /* Raise error on expressions and declarations with an implied 'any' type. */ // "strictNullChecks": true, /* Enable strict null checks. */ // "strictFunctionTypes": true, /* Enable strict checking of function types. */ // "strictBindCallApply": true, /* Enable strict 'bind', 'call', and 'apply' methods on functions. */ // "strictPropertyInitialization": true, /* Enable strict checking of property initialization in classes. */ // "noImplicitThis": true, /* Raise error on 'this' expressions with an implied 'any' type. */ // "alwaysStrict": true, /* Parse in strict mode and emit "use strict" for each source file. */ /* Additional Checks */ // "noUnusedLocals": true, /* Report errors on unused locals. */ // "noUnusedParameters": true, /* Report errors on unused parameters. */ // "noImplicitReturns": true, /* Report error when not all code paths in function return a value. */ // "noFallthroughCasesInSwitch": true, /* Report errors for fallthrough cases in switch statement. */ // "noUncheckedIndexedAccess": true, /* Include 'undefined' in index signature results */ // "noImplicitOverride": true, /* Ensure overriding members in derived classes are marked with an 'override' modifier. */ // "noPropertyAccessFromIndexSignature": true, /* Require undeclared properties from index signatures to use element accesses. */ /* Module Resolution Options */ "moduleResolution": "node", /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */ // "baseUrl": "./", /* Base directory to resolve non-absolute module names. */ // "paths": {}, /* A series of entries which re-map imports to lookup locations relative to the 'baseUrl'. */ // "rootDirs": [], /* List of root folders whose combined content represents the structure of the project at runtime. */ "typeRoots": [ "./node_modules/@types", ], /* List of folders to include type definitions from. */ // "types": [], /* Type declaration files to be included in compilation. */ // "allowSyntheticDefaultImports": true, /* Allow default imports from modules with no default export. This does not affect code emit, just typechecking. */ "esModuleInterop": true, /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */ // "preserveSymlinks": true, /* Do not resolve the real path of symlinks. */ // "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */ /* Experimental Options */ // "experimentalDecorators": true, /* Enables experimental support for ES7 decorators. */ // "emitDecoratorMetadata": true, /* Enables experimental support for emitting type metadata for decorators. */ /* Advanced Options */ "skipLibCheck": true, /* Skip type checking of declaration files. */ "forceConsistentCasingInFileNames": true, /* Disallow inconsistently-cased references to the same file. */ "noUncheckedIndexedAccess": true }, "exclude": [ "node_modules", ".build" ] }