├── .nvmrc ├── src ├── index.ts ├── get.ts └── get.test.ts ├── .gitignore ├── .npmignore ├── .travis.yml ├── jest.config.js ├── stryker.conf.js ├── .eslintrc.js ├── LICENSE ├── package.json ├── README.md └── tsconfig.json /.nvmrc: -------------------------------------------------------------------------------- 1 | node -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./get"; 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | coverage 2 | dist 3 | yarn-error.log 4 | node_modules 5 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | coverage 2 | dist 3 | yarn-error.log 4 | node_modules 5 | dist/*.test.* -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | install: 3 | - yarn install --frozen-lockfile 4 | script: 5 | - yarn build 6 | - yarn lint 7 | - yarn type-coverage 8 | - yarn test 9 | - yarn stryker run 10 | -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | "roots": [ 3 | "/src" 4 | ], 5 | "transform": { 6 | "^.+\\.tsx?$": "ts-jest" 7 | }, 8 | "collectCoverage": true, 9 | "coverageThreshold": { 10 | "global": { 11 | "branches": 100, 12 | "functions": 100, 13 | "lines": 100, 14 | "statements": 100 15 | } 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /stryker.conf.js: -------------------------------------------------------------------------------- 1 | module.exports = function(config) { 2 | config.set({ 3 | mutator: "typescript", 4 | packageManager: "yarn", 5 | reporters: ["clear-text", "progress"], 6 | testRunner: "jest", 7 | transpilers: [], 8 | coverageAnalysis: "off", 9 | tsconfigFile: "tsconfig.json", 10 | mutate: ["src/**/*.ts", "!src/**/*.test.ts"] 11 | }); 12 | }; 13 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | parser: "@typescript-eslint/parser", 3 | parserOptions: { 4 | project: "./tsconfig.json", 5 | ecmaVersion: 2018, 6 | sourceType: "module" 7 | }, 8 | extends: [ 9 | "typed-fp", 10 | "plugin:sonarjs/recommended", 11 | "plugin:jest/recommended", 12 | "prettier/@typescript-eslint", 13 | "plugin:prettier/recommended" 14 | ], 15 | env: { 16 | "jest/globals": true, 17 | es6: true 18 | }, 19 | plugins: ["jest", "sonarjs", "functional", "@typescript-eslint", "prettier", "total-functions"], 20 | rules: {} 21 | }; 22 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Daniel Nixon 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "total-functions", 3 | "version": "3.0.0", 4 | "description": "A collection of total functions to replace TypeScript's built-in partial functions.", 5 | "main": "dist", 6 | "repository": "https://github.com/danielnixon/total-functions.git", 7 | "author": "Daniel Nixon ", 8 | "license": "MIT", 9 | "devDependencies": { 10 | "@stryker-mutator/core": "^3.3.1", 11 | "@stryker-mutator/jest-runner": "^3.3.1", 12 | "@stryker-mutator/typescript": "^3.3.1", 13 | "@types/jest": "^26.0.16", 14 | "@typescript-eslint/eslint-plugin": "^4.9.0", 15 | "@typescript-eslint/experimental-utils": "^4.5.0", 16 | "@typescript-eslint/parser": "^4.9.0", 17 | "eslint": "^7.14.0", 18 | "eslint-config-prettier": "^6.15.0", 19 | "eslint-config-typed-fp": "^0.10.0", 20 | "eslint-plugin-functional": "^3.1.0", 21 | "eslint-plugin-jest": "^24.1.3", 22 | "eslint-plugin-prettier": "^3.2.0", 23 | "eslint-plugin-sonarjs": "^0.5.0", 24 | "eslint-plugin-total-functions": "^3.3.0", 25 | "jest": "^26.6.3", 26 | "prettier": "^2.2.1", 27 | "ts-jest": "^26.4.4", 28 | "type-coverage": "^2.14.6", 29 | "typescript": "^4.1.2" 30 | }, 31 | "scripts": { 32 | "build": "tsc", 33 | "lint": "eslint src --ext .ts,.tsx", 34 | "format": "prettier --write 'src/**/*.{ts,tsx}'", 35 | "test": "jest", 36 | "release": "yarn build && yarn lint && yarn type-coverage && yarn publish" 37 | }, 38 | "typeCoverage": { 39 | "atLeast": 99.56, 40 | "strict": true, 41 | "detail": true 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/get.ts: -------------------------------------------------------------------------------- 1 | export type ArrayIndexReturnValue< 2 | A extends ArrayLike, 3 | I extends PropertyKey 4 | // If this is a tuple we don't need to add undefined to the return type, 5 | // but if it's just a plain old array we have to add undefined to the return type. 6 | // This also catches negative indices passed to tuples. 7 | > = I extends number 8 | ? A[number] extends A[I] 9 | ? number extends A["length"] 10 | ? A[I] | undefined // we don't have a defined length - have to include undefined 11 | : undefined // we have a defined length - this must be undefined 12 | : number extends A["length"] 13 | ? A[I] | undefined // Semi-tuple - need undefined. TODO: exclude undefined for the tuple portions of this array 14 | : A[I] // Tuple - don't need undefined 15 | : undefined; 16 | 17 | export type GetReturnType< 18 | // eslint-disable-next-line @typescript-eslint/ban-types 19 | A extends Record | ArrayLike, 20 | I extends A extends ArrayLike ? ArrayIndex : keyof A 21 | > = A extends ArrayLike 22 | ? ArrayIndexReturnValue 23 | : A extends { readonly [i in I]: unknown } 24 | ? A[I] 25 | : A[I] | undefined; 26 | 27 | type ArrayIndex> = number extends A["length"] 28 | ? number 29 | : 0 extends A["length"] 30 | ? never 31 | : 1 extends A["length"] 32 | ? 0 33 | : 2 extends A["length"] 34 | ? 0 | 1 35 | : 3 extends A["length"] 36 | ? 0 | 1 | 2 37 | : 4 extends A["length"] 38 | ? 0 | 1 | 2 | 3 39 | : 5 extends A["length"] 40 | ? 0 | 1 | 2 | 3 | 4 41 | : 6 extends A["length"] 42 | ? 0 | 1 | 2 | 3 | 4 | 5 43 | : 7 extends A["length"] 44 | ? 0 | 1 | 2 | 3 | 4 | 5 | 6 45 | : 8 extends A["length"] 46 | ? 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 47 | : 9 extends A["length"] 48 | ? 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 49 | : number; 50 | 51 | /** 52 | * A total function (one that doesn't lie about the possibility of returning undefined) 53 | * to replace the default (partial) array index operator. 54 | * 55 | * @see https://github.com/Microsoft/TypeScript/issues/13778 56 | */ 57 | export const get = < 58 | // eslint-disable-next-line @typescript-eslint/ban-types 59 | A extends Record | ArrayLike, 60 | I extends A extends ArrayLike ? ArrayIndex : keyof A 61 | >( 62 | a: A, 63 | i: I 64 | // eslint-disable-next-line @typescript-eslint/consistent-type-assertions, total-functions/no-unsafe-subscript 65 | ): GetReturnType => a[i] as GetReturnType; 66 | 67 | /** 68 | * An escape hatch for when you can't make the types line up in `get` and are willing 69 | * to accept undefined always being included in the return type. 70 | */ 71 | export const getOrUndefined = ( 72 | a: A, 73 | i: I 74 | ): A[I] | undefined => a[i]; 75 | -------------------------------------------------------------------------------- /src/get.test.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/ban-ts-comment */ 2 | /* eslint-disable functional/functional-parameters */ 3 | /* eslint-disable functional/no-expression-statement */ 4 | 5 | import { get, getOrUndefined } from "./get"; 6 | 7 | describe("get", () => { 8 | it("provides a safe alternative to array subscript access", () => { 9 | // tuple 10 | const xs = [1, 2, 3] as const; 11 | 12 | expect<1>(get(xs, 0)).toBe(1); 13 | expect<2>(get(xs, 1)).toBe(2); 14 | expect<3>(get(xs, 2)).toBe(3); 15 | // @ts-expect-error 16 | get(xs, 100); 17 | // @ts-expect-error 18 | get(xs, -1); 19 | // @ts-expect-error 20 | get(xs, "length"); 21 | 22 | // array 23 | const ys: readonly number[] = [1, 2, 3]; 24 | expect(get(ys, 1)).toBe(2); 25 | expect(get(ys, 100)).toBe(undefined); 26 | expect(get(ys, -1)).toBe(undefined); 27 | 28 | // sparse array 29 | // eslint-disable-next-line no-sparse-arrays 30 | const zs: readonly (number | undefined)[] = [1, , 2, 3]; 31 | expect(get(zs, 1)).toBe(undefined); 32 | expect(get(zs, 100)).toBe(undefined); 33 | expect(get(zs, -1)).toBe(undefined); 34 | 35 | // readonly array 36 | const as: ReadonlyArray<1 | 2 | 3> = [1, 2, 3]; 37 | expect<1 | 2 | 3 | undefined>(get(as, 1)).toBe(2); 38 | expect<1 | 2 | 3 | undefined>(get(as, 100)).toBe(undefined); 39 | expect<1 | 2 | 3 | undefined>(get(as, -1)).toBe(undefined); 40 | 41 | // semi-tuple 42 | const bs: readonly [number, ...(readonly string[])] = [1, "a", "b"]; 43 | expect(get(bs, 0)).toBe(1); // TODO: can we exclude undefined here? 44 | expect(get(bs, 1)).toBe("a"); 45 | expect(get(bs, 100)).toBe(undefined); 46 | expect(get(bs, -1)).toBe(undefined); 47 | // @ts-expect-error 48 | get(bs, "length"); 49 | 50 | // record 51 | // eslint-disable-next-line @typescript-eslint/ban-types 52 | const record: Record = { 1: "asdf" }; 53 | expect(get(record, 1)).toBe("asdf"); 54 | expect(get(record, 100)).toBe(undefined); 55 | 56 | // object 57 | const obj = { 1: "asdf" }; 58 | expect(get(obj, 1)).toBe("asdf"); 59 | // @ts-expect-error 60 | get(obj, 100); 61 | 62 | // const object 63 | const constObj = { 1: "asdf" } as const; 64 | expect<"asdf">(get(constObj, 1)).toBe("asdf"); 65 | // @ts-expect-error 66 | get(constObj, 100); 67 | 68 | // string 69 | const str = "foo" as const; 70 | expect(get(str, 1)).toBe("o"); 71 | }); 72 | }); 73 | 74 | describe("getOrUndefined", () => { 75 | it("provides a safe alternative to array subscript access", () => { 76 | const xs: readonly number[] = [1, 2, 3]; 77 | expect(getOrUndefined(xs, 1)).toBe(2); 78 | expect(getOrUndefined(xs, 100)).toBe(undefined); 79 | expect(getOrUndefined(xs, -1)).toBe(undefined); 80 | }); 81 | }); 82 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # TypeScript Total Functions 2 | 3 | [![Build Status](https://travis-ci.org/danielnixon/total-functions.svg?branch=master)](https://travis-ci.org/danielnixon/total-functions) 4 | [![type-coverage](https://img.shields.io/badge/dynamic/json.svg?label=type-coverage&prefix=%E2%89%A5&suffix=%&query=$.typeCoverage.atLeast&uri=https%3A%2F%2Fraw.githubusercontent.com%2Fdanielnixon%2Ftotal-functions%2Fmaster%2Fpackage.json)](https://github.com/plantain-00/type-coverage) 5 | [![Known Vulnerabilities](https://snyk.io/test/github/danielnixon/total-functions/badge.svg?targetFile=package.json)](https://snyk.io/test/github/danielnixon/total-functions?targetFile=package.json) 6 | [![npm](https://img.shields.io/npm/v/total-functions.svg)](https://www.npmjs.com/package/total-functions) 7 | 8 | [![dependencies Status](https://david-dm.org/danielnixon/total-functions/status.svg)](https://david-dm.org/danielnixon/total-functions) 9 | [![devDependencies Status](https://david-dm.org/danielnixon/total-functions/dev-status.svg)](https://david-dm.org/danielnixon/total-functions?type=dev) 10 | 11 | A collection of total functions to replace TypeScript's built-in [partial functions](https://wiki.haskell.org/Partial_functions). 12 | 13 | Intended to be used with [strictNullChecks](https://www.typescriptlang.org/docs/handbook/compiler-options.html) enabled. 14 | 15 | ## Installation 16 | 17 | ```sh 18 | # yarn 19 | yarn add total-functions 20 | 21 | # npm 22 | npm install total-functions 23 | ``` 24 | 25 | ## The Functions 26 | 27 | ### `get` (type-safe member and indexed access operator) 28 | 29 | Prior to TypeScript 4.1's [noUncheckedIndexedAccess](https://devblogs.microsoft.com/typescript/announcing-typescript-4-1-beta/#no-unchecked-indexed-access) option, member access for arrays and records was not type safe. For example: 30 | 31 | ```typescript 32 | const a: string[] = []; 33 | const b = a[0]; // b has type string, not string | undefined as you might expect 34 | b.toUpperCase(); // This explodes at runtime 35 | 36 | const record: Record = { foo: "foo" }; 37 | const bar = record["bar"]; // bar has type string, not string | undefined 38 | bar.toUpperCase(); // This explodes at runtime 39 | 40 | const baz = record.baz; // baz has type string, not string | undefined 41 | baz.toUpperCase(); // This explodes at runtime 42 | 43 | const str = ""; 44 | const s = str[0]; // s has type string, not string | undefined 45 | s.toUpperCase(); // This explodes at runtime 46 | ``` 47 | 48 | `get` is a safe alternative: 49 | 50 | ```typescript 51 | import { get } from "total-functions"; 52 | 53 | const b = get(a, 0); // b has type object | undefined 54 | 55 | const bar = get(record, "bar"); // bar has type string | undefined 56 | ``` 57 | 58 | Note that `get` will exclude `undefined` from the return type when there is enough type information to be confident that the result cannot be undefined. See the object and tuple examples below for examples where `undefined` is not included in the return type. 59 | 60 | More usage examples: 61 | 62 | ```typescript 63 | // tuple 64 | const xs = [1, 2, 3] as const; 65 | const x1 = get(xs, 1); // 2 66 | const x100 = get(xs, 100); // undefined 67 | const xMinus1 = get(xs, -1); // undefined 68 | xs.map(x => x /* 1 | 2 | 3 */); 69 | 70 | // array 71 | const ys = [1, 2, 3]; 72 | const y1 = get(ys, 1); // number | undefined 73 | const y100 = get(ys, 100); // number | undefined 74 | ys.map(y => y /* number */); 75 | 76 | // sparse array 77 | const zs = [1, , 2, 3]; 78 | const z1 = get(zs, 1); // number | undefined 79 | const z100 = get(zs, 100); // number | undefined 80 | zs.map(z => z /* number | undefined */); 81 | 82 | // readonly array 83 | const as: ReadonlyArray<1 | 2 | 3> = [1, 2, 3]; 84 | const a1 = get(as, 1); // 1 | 2 | 3 | undefined 85 | const a100 = get(as, 100); // 1 | 2 | 3 | undefined 86 | 87 | // record 88 | const record: Record = { 1: "asdf" }; 89 | const record1 = get(record, 1); // string | undefined 90 | const record100 = get(record, 100); // string | undefined 91 | 92 | // object 93 | const obj = { 1: "asdf" }; 94 | const obj1 = get(obj, 1); // string 95 | const obj100 = get(obj, 100); // doesn't compile 96 | 97 | // const object 98 | const constObj = { 1: "asdf" } as const; 99 | const constObj1 = get(constObj, 1); // "asdf" 100 | const constObj100 = get(constObj, 100); // doesn't compile 101 | ``` 102 | 103 | You only need to use this if you are stuck on Typescript < 4.1. 104 | 105 | ## ESLint 106 | 107 | There's a corresponding ESLint plugin to enforce the use of `noUncheckedIndexedAccess` and/or ban the partial functions replaced by this library. 108 | 109 | See https://github.com/danielnixon/eslint-plugin-total-functions 110 | 111 | # See Also 112 | * [TypeScript for Functional Programmers](https://www.typescriptlang.org/docs/handbook/typescript-in-5-minutes-func.html) 113 | * https://github.com/danielnixon/readonly-types 114 | * https://github.com/danielnixon/eslint-plugin-total-functions 115 | * https://github.com/jonaskello/eslint-plugin-functional 116 | * https://github.com/danielnixon/eslint-config-typed-fp 117 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | /* Basic Options */ 4 | // "incremental": true, /* Enable incremental compilation */ 5 | "target": "es2015", /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019' or 'ESNEXT'. */ 6 | "module": "commonjs", /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', or 'ESNext'. */ 7 | // "lib": [], /* Specify library files to be included in the compilation. */ 8 | // "allowJs": true, /* Allow javascript files to be compiled. */ 9 | // "checkJs": true, /* Report errors in .js files. */ 10 | // "jsx": "preserve", /* Specify JSX code generation: 'preserve', 'react-native', or 'react'. */ 11 | "declaration": true, /* Generates corresponding '.d.ts' file. */ 12 | // "declarationMap": true, /* Generates a sourcemap for each corresponding '.d.ts' file. */ 13 | // "sourceMap": true, /* Generates corresponding '.map' file. */ 14 | // "outFile": "./", /* Concatenate and emit output to single file. */ 15 | "outDir": "dist", /* Redirect output structure to the directory. */ 16 | // "rootDir": "./", /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */ 17 | // "composite": true, /* Enable project compilation */ 18 | // "tsBuildInfoFile": "./", /* Specify file to store incremental compilation information */ 19 | // "removeComments": true, /* Do not emit comments to output. */ 20 | // "noEmit": true, /* Do not emit outputs. */ 21 | // "importHelpers": true, /* Import emit helpers from 'tslib'. */ 22 | // "downlevelIteration": true, /* Provide full support for iterables in 'for-of', spread, and destructuring when targeting 'ES5' or 'ES3'. */ 23 | // "isolatedModules": true, /* Transpile each file as a separate module (similar to 'ts.transpileModule'). */ 24 | 25 | /* Strict Type-Checking Options */ 26 | "strict": true, /* Enable all strict type-checking options. */ 27 | "noImplicitAny": true, /* Raise error on expressions and declarations with an implied 'any' type. */ 28 | "strictNullChecks": true, /* Enable strict null checks. */ 29 | "strictFunctionTypes": true, /* Enable strict checking of function types. */ 30 | "strictBindCallApply": true, /* Enable strict 'bind', 'call', and 'apply' methods on functions. */ 31 | "strictPropertyInitialization": true, /* Enable strict checking of property initialization in classes. */ 32 | "noImplicitThis": true, /* Raise error on 'this' expressions with an implied 'any' type. */ 33 | "alwaysStrict": true, /* Parse in strict mode and emit "use strict" for each source file. */ 34 | 35 | /* Additional Checks */ 36 | "noUnusedLocals": true, /* Report errors on unused locals. */ 37 | "noUnusedParameters": true, /* Report errors on unused parameters. */ 38 | "noImplicitReturns": true, /* Report error when not all code paths in function return a value. */ 39 | "noFallthroughCasesInSwitch": true, /* Report errors for fallthrough cases in switch statement. */ 40 | 41 | /* Module Resolution Options */ 42 | // "moduleResolution": "node", /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */ 43 | // "baseUrl": "./", /* Base directory to resolve non-absolute module names. */ 44 | // "paths": {}, /* A series of entries which re-map imports to lookup locations relative to the 'baseUrl'. */ 45 | // "rootDirs": [], /* List of root folders whose combined content represents the structure of the project at runtime. */ 46 | // "typeRoots": [], /* List of folders to include type definitions from. */ 47 | // "types": [], /* Type declaration files to be included in compilation. */ 48 | // "allowSyntheticDefaultImports": true, /* Allow default imports from modules with no default export. This does not affect code emit, just typechecking. */ 49 | "esModuleInterop": true /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */ 50 | // "preserveSymlinks": true, /* Do not resolve the real path of symlinks. */ 51 | // "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */ 52 | 53 | /* Source Map Options */ 54 | // "sourceRoot": "", /* Specify the location where debugger should locate TypeScript files instead of source locations. */ 55 | // "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */ 56 | // "inlineSourceMap": true, /* Emit a single file with source maps instead of having a separate file. */ 57 | // "inlineSources": true, /* Emit the source alongside the sourcemaps within a single file; requires '--inlineSourceMap' or '--sourceMap' to be set. */ 58 | 59 | /* Experimental Options */ 60 | // "experimentalDecorators": true, /* Enables experimental support for ES7 decorators. */ 61 | // "emitDecoratorMetadata": true, /* Enables experimental support for emitting type metadata for decorators. */ 62 | } 63 | } 64 | --------------------------------------------------------------------------------