├── .gitignore ├── .travis.yml ├── LICENSE ├── README.md ├── index.ts ├── package.json ├── test.ts ├── tsconfig.json └── wallaby.js /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | 8 | # Runtime data 9 | pids 10 | *.pid 11 | *.seed 12 | *.pid.lock 13 | 14 | # Directory for instrumented libs generated by jscoverage/JSCover 15 | lib-cov 16 | 17 | # Coverage directory used by tools like istanbul 18 | coverage 19 | 20 | # nyc test coverage 21 | .nyc_output 22 | 23 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 24 | .grunt 25 | 26 | # Bower dependency directory (https://bower.io/) 27 | bower_components 28 | 29 | # node-waf configuration 30 | .lock-wscript 31 | 32 | # Compiled binary addons (http://nodejs.org/api/addons.html) 33 | build/Release 34 | 35 | # Dependency directories 36 | node_modules/ 37 | jspm_packages/ 38 | 39 | # Typescript v1 declaration files 40 | typings/ 41 | 42 | # Optional npm cache directory 43 | .npm 44 | 45 | # Optional eslint cache 46 | .eslintcache 47 | 48 | # Optional REPL history 49 | .node_repl_history 50 | 51 | # Output of 'npm pack' 52 | *.tgz 53 | 54 | # Yarn Integrity file 55 | .yarn-integrity 56 | 57 | # dotenv environment variables file 58 | .env 59 | 60 | *.d.ts 61 | *.js 62 | *.js.map 63 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - "4" 4 | - "6" 5 | - "8" 6 | - "9" -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Tim Perry 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 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # typesafe-get [![Travis Build Status](https://img.shields.io/travis/pimterry/typesafe-get.svg)](https://travis-ci.org/pimterry/typesafe-get) [![Uses TypeScript](https://img.shields.io/badge/%3C%2F%3E-TypeScript-blue.svg)](http://typescriptlang.org) 2 | 3 | A typesafe way to access nested properties through potentially-undefined parent properties, while we wait for the [optional chaining ?. operator](https://tc39.github.io/proposal-optional-chaining/) to finally exist and get [real TypeScript support](https://github.com/microsoft/TypeScript/issues/16#issuecomment-55315658). 4 | 5 | With typesafe-get, the below TypeScript code will work, without no compile or runtime errors, for all valid values of `input`: 6 | 7 | ```ts 8 | let input: { a: { b?: { c: null | { d: string } } } } | undefined = ...; 9 | let result: string | undefined = get(input, 'a', 'b', 'c', 'd'); 10 | ``` 11 | 12 | All parameters are validated as properties at the relevant level, and 13 | the return type will always be `finalPropType | undefined` (the type of the property 14 | at the very last level, or undefined). 15 | 16 | If at any point while following the chain you attempt to follow a path through an 17 | undefined object then undefined is immediately returned, and you'll never see the dreaded 18 | `Cannot read property '' of undefined` exception. It works with simple iteration 19 | over the property names provided, no crazy (and expensive) try/catch magic here. 20 | 21 | ## Install it 22 | 23 | ``` 24 | npm install --save typesafe-get 25 | ``` 26 | 27 | ## Use it 28 | 29 | This module ships with TypeScript types, and is built in JS for UMD, so you should 30 | be able to immediately start using it in many environments with no further setup. 31 | 32 | With some webpack configurations, you'll need to explicitly allow requiring UMD modules. 33 | To do that you can use [UMD-Compat-Loader](https://www.npmjs.com/package/umd-compat-loader), 34 | like so: 35 | 36 | ```js 37 | module: { 38 | rules: [ 39 | // ...other rules here 40 | { 41 | // You can use (typesafe-get|other-module) if you're using multiple UMD modules 42 | test: /node_modules[\\|/]typesafe-get/, 43 | use: { loader: 'umd-compat-loader' } 44 | } 45 | ] 46 | } 47 | ``` 48 | 49 | The `get` function is exported as both a property and the default export of this module. 50 | That means importing looks like: 51 | 52 | ```ts 53 | import { get } from 'typesafe-get'; 54 | // or 55 | import get from 'typesafe-get'; 56 | ``` 57 | 58 | To use `get`: 59 | 60 | ```ts 61 | // Equivalent to obj.aPropertyKey 62 | get(obj, 'aPropertyKey'); 63 | 64 | // Equivalent to obj.key1.key2, but returning undefined if obj.key1 is undefined: 65 | get(obj, 'key1', 'key2'); 66 | 67 | // Equivalent to obj.key1.key2.key3.key4.key5, but returning undefined if any step en route is undefined: 68 | get(obj, 'key1', 'key2', 'key3', 'key4', 'key5'); 69 | 70 | // Equivalent to array[0].key1, but returning undefined if any step en route is undefined: 71 | get(array, 0, 'key1'); 72 | ``` 73 | 74 | Each parameter name is checked against the valid parameter names at that level (so a parameter 75 | name that definitely doesn't exist will be caught by TS). 76 | 77 | The return type will be automatically inferred as the type the final parameter would have, 78 | if the whole chain is defined, or `undefined`. 79 | 80 | `get` itself supports up to 5 property parameters max. If you really truly honestly need more than that, 81 | take a good hard look at yourself, and then feel free to open a PR - it should be fairly easy to see 82 | how to extend the types to support more. But only if you're _really_ sure you need this. Seriously, 83 | what are you doing that makes this a good idea. 84 | 85 | ## Contributing 86 | 87 | Have a bug? File an issue with a simple example that reproduces this so we can take a look & confirm. 88 | 89 | Want to make a change? Submit a PR, explain why it's useful, and make sure you've updated the docs 90 | (this file) and the tests (see `test.ts`). You can run the tests with `npm test`. 91 | -------------------------------------------------------------------------------- /index.ts: -------------------------------------------------------------------------------- 1 | // The value of T[S], if it's not null/undefined 2 | export type Prop = NonNullable; 3 | 4 | export function get< 5 | T, 6 | S1 extends keyof NonNullable, 7 | S2 extends keyof Prop, S1>, 8 | S3 extends keyof Prop, S1>, S2>, 9 | S4 extends keyof Prop, S1>, S2>, S3>, 10 | S5 extends keyof Prop, S1>, S2>, S3>, S4> 11 | >( 12 | obj: T, 13 | prop1: S1, 14 | prop2: S2, 15 | prop3: S3, 16 | prop4: S4, 17 | prop5: S5, 18 | ): Prop, S1>, S2>, S3>, S4>[S5] | undefined; 19 | 20 | export function get< 21 | T, 22 | S1 extends keyof NonNullable, 23 | S2 extends keyof Prop, S1>, 24 | S3 extends keyof Prop, S1>, S2>, 25 | S4 extends keyof Prop, S1>, S2>, S3> 26 | >( 27 | obj: T, 28 | prop1: S1, 29 | prop2: S2, 30 | prop3: S3, 31 | prop4: S4, 32 | ): Prop, S1>, S2>, S3>[S4] | undefined; 33 | 34 | export function get< 35 | T, 36 | S1 extends keyof NonNullable, 37 | S2 extends keyof Prop, S1>, 38 | S3 extends keyof Prop, S1>, S2> 39 | >( 40 | obj: T, 41 | prop1: S1, 42 | prop2: S2, 43 | prop3: S3, 44 | ): Prop, S1>, S2>[S3] | undefined; 45 | 46 | export function get< 47 | T, 48 | S1 extends keyof NonNullable, 49 | S2 extends keyof Prop, S1> 50 | >( 51 | obj: T, 52 | prop1: S1, 53 | prop2: S2 54 | ): Prop, S1>[S2] | undefined; 55 | 56 | export function get< 57 | T, 58 | S1 extends keyof NonNullable 59 | >( 60 | obj: T, 61 | prop1: S1 62 | ): NonNullable[S1] | undefined; 63 | 64 | export function get( 65 | obj: T, 66 | ...props: string[] 67 | ): any | undefined { 68 | let value: any = obj; 69 | 70 | while (props.length > 0) { 71 | if (value == null) return undefined; 72 | 73 | let nextProp = props.shift(); 74 | value = value[nextProp]; 75 | } 76 | 77 | return value; 78 | } 79 | 80 | export default get; -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "typesafe-get", 3 | "version": "2.1.2", 4 | "description": "A typesafe way to get nested properties when any parent property might be undefined, while we wait for the optional chaining operator to finally exist", 5 | "main": "index.js", 6 | "types": "index.d.ts", 7 | "files": [ 8 | "index.js", 9 | "index.js.map", 10 | "index.d.ts", 11 | "index.ts" 12 | ], 13 | "scripts": { 14 | "test": "mocha -r ts-node/register test.ts", 15 | "prepublishOnly": "tsc" 16 | }, 17 | "repository": { 18 | "type": "git", 19 | "url": "git+https://github.com/pimterry/typesafe-get.git" 20 | }, 21 | "keywords": [ 22 | "typescript", 23 | "typesafe", 24 | "get", 25 | "optional", 26 | "chaining", 27 | "optional chaining", 28 | "lookup", 29 | "property", 30 | "undefined", 31 | "null", 32 | "safe" 33 | ], 34 | "author": "Tim Perry ", 35 | "license": "MIT", 36 | "bugs": { 37 | "url": "https://github.com/pimterry/typesafe-get/issues" 38 | }, 39 | "homepage": "https://github.com/pimterry/typesafe-get#readme", 40 | "devDependencies": { 41 | "@types/chai": "^4.1.2", 42 | "@types/mocha": "^2.2.48", 43 | "chai": "^4.1.2", 44 | "mocha": "^5.0.1", 45 | "ts-node": "^7.0.0", 46 | "typescript": "^2.9.2" 47 | }, 48 | "dependencies": {} 49 | } 50 | -------------------------------------------------------------------------------- /test.ts: -------------------------------------------------------------------------------- 1 | import { get } from './index'; 2 | import { expect } from 'chai'; 3 | 4 | describe("typesafe-get", () => { 5 | it("allows you to get a set top-level property", () => { 6 | let result = get({ result: 'correct' }, 'result'); 7 | expect(result).to.equal('correct'); 8 | }); 9 | 10 | it("correctly infers the type of a queried property", () => { 11 | let result = get({ result: { value: 'neat' } }, 'result'); 12 | expect(result!.value).to.equal('neat'); 13 | }); 14 | 15 | it("lets you look up possibly undefined properties", () => { 16 | const input: { a?: string } = { }; 17 | expect(get(input, 'a')).to.equal(undefined); 18 | }); 19 | 20 | it("lets you look up properties on a possibly undefined object", () => { 21 | let input: { a: string } | undefined; 22 | expect(get(input, 'a')).to.equal(undefined); 23 | }); 24 | 25 | it("lets you look up nested properties", () => { 26 | const input = { a: { b: 1 } }; 27 | expect(get(input, 'a', 'b')).to.equal(1); 28 | }); 29 | 30 | it("lets you look up nested properties that may be undefined", () => { 31 | const input: { a?: { b: number } } = { }; 32 | let result = get(input, 'a', 'b'); 33 | expect(result).to.equal(undefined); 34 | }); 35 | 36 | it("lets you look up nested properties that may be undefined on a possibly undefined object", () => { 37 | let input: { a: { b: number } } | undefined; 38 | let result = get(input, 'a', 'b'); 39 | expect(result).to.deep.equal(undefined); 40 | }); 41 | 42 | it("correctly infers the type of a nested possibly-null property", () => { 43 | const input: { a?: { b: { c: number } } } = { a: { b: { c: 5 } } }; 44 | let result = get(input, 'a', 'b'); 45 | expect(result!.c).to.equal(5); 46 | }); 47 | 48 | it("lets you lookup through array indexes", () => { 49 | const input = { 50 | x: [ 51 | { a: { b: 'abc' } }, 52 | { a: { b: 'def' } } 53 | ] 54 | }; 55 | 56 | let result = get(input, 'x', 0, 'a'); 57 | 58 | expect(result!.b).to.equal('abc'); 59 | }); 60 | 61 | it("returns null if the final property is null", () => { 62 | const input: { a: number | null } = { a: null }; 63 | let result = get(input, 'a'); 64 | expect(result).to.equal(null); 65 | }); 66 | 67 | it("returns undefined if following a path through a null", () => { 68 | const input: { a: { b: number } | null } = { a: null }; 69 | let result = get(input, 'a', 'b'); 70 | expect(result).to.equal(undefined); 71 | }); 72 | 73 | it("allows doubly nested lookup through undefined properties", () => { 74 | const input: { 75 | a?: { b?: { c?: number } } 76 | } = {}; 77 | 78 | let result = get(input, 'a', 'b', 'c'); 79 | 80 | expect(result).to.equal(undefined); 81 | }); 82 | 83 | it("allows quadruply nested lookup through undefined properties on a possibly undefined object", () => { 84 | let input: { a?: { b?: { c?: number } } } | undefined; 85 | 86 | let result = get(input, 'a', 'b', 'c'); 87 | 88 | expect(result).to.deep.equal(undefined); 89 | }); 90 | 91 | it("allows doubly nested lookup through defined properties", () => { 92 | const input = { 93 | a: { b: { c: 123 } } 94 | }; 95 | 96 | let result = get(input, 'a', 'b', 'c'); 97 | 98 | expect(result).to.equal(123); 99 | }); 100 | 101 | it("allows triply nested lookup through undefined properties", () => { 102 | const input: { 103 | a?: { b?: { c?: { d?: string } } } 104 | } = {}; 105 | 106 | let result = get(input, 'a', 'b', 'c', 'd'); 107 | 108 | expect(result).to.equal(undefined); 109 | }); 110 | 111 | it("allows triply nested lookup through defined properties", () => { 112 | const input = { 113 | a: { b: { c: { d: 'hello' } } } 114 | }; 115 | 116 | let result = get(input, 'a', 'b', 'c', 'd'); 117 | 118 | expect(result).to.equal('hello'); 119 | }); 120 | 121 | it("allows quadruply nested lookup through undefined properties", () => { 122 | const input: { 123 | a?: { b?: { c?: { d?: { e?: {} } } } } 124 | } = {}; 125 | 126 | let result = get(input, 'a', 'b', 'c', 'd', 'e'); 127 | 128 | expect(result).to.equal(undefined); 129 | }); 130 | 131 | it("allows quadruply nested lookup through undefined properties on a possibly undefined object", () => { 132 | let input: { 133 | a?: { b?: { c?: { d?: { e?: {} } } } } 134 | } | undefined; 135 | 136 | let result = get(input, 'a', 'b', 'c', 'd', 'e'); 137 | 138 | expect(result).to.deep.equal(undefined); 139 | }); 140 | 141 | it("allows quadruply nested lookup through defined properties", () => { 142 | const input = { 143 | a: { b: { c: { d: { e: {} } } } } 144 | }; 145 | 146 | let result = get(input, 'a', 'b', 'c', 'd', 'e'); 147 | 148 | expect(result).to.deep.equal({}); 149 | }); 150 | }); -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "module": "umd", 5 | "outDir": ".", 6 | "declaration": true, 7 | "sourceMap": true, 8 | "strict": true 9 | }, 10 | "compileOnSave": true, 11 | "buildOnSave": true, 12 | "files": [ 13 | "index.ts", 14 | "test.ts" 15 | ] 16 | } 17 | -------------------------------------------------------------------------------- /wallaby.js: -------------------------------------------------------------------------------- 1 | module.exports = (wallaby) => { 2 | process.env.NODE_EXTRA_CA_CERTS = './test/fixtures/test-ca.pem' 3 | 4 | return { 5 | files: [ 6 | 'package.json', 7 | 'index.ts', 8 | '!test.ts' 9 | ], 10 | tests: [ 11 | 'test.ts' 12 | ], 13 | 14 | workers: { 15 | initial: 4, 16 | regular: 1, 17 | restart: true 18 | }, 19 | 20 | testFramework: 'mocha', 21 | env: { 22 | type: 'node' 23 | }, 24 | debug: true 25 | }; 26 | }; --------------------------------------------------------------------------------