Initial commit to ts-oc repo (#1)

This commit is contained in:
Neville Bowers
2018-08-08 23:14:01 -07:00
committed by GitHub
parent d7b7b42c6c
commit e276562781
11 changed files with 5758 additions and 1 deletions

3
.github/PULL_REQUEST_TEMPLATE.md vendored Normal file
View File

@@ -0,0 +1,3 @@
## Description
## Test Plan

64
.gitignore vendored Normal file
View File

@@ -0,0 +1,64 @@
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
# Runtime data
pids
*.pid
*.seed
*.pid.lock
# Directory for instrumented libs generated by jscoverage/JSCover
lib-cov
# Coverage directory used by tools like istanbul
coverage
# nyc test coverage
.nyc_output
# Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files)
.grunt
# Bower dependency directory (https://bower.io/)
bower_components
# node-waf configuration
.lock-wscript
# Compiled binary addons (http://nodejs.org/api/addons.html)
build/Release
# Dependency directories
node_modules/
jspm_packages/
# Typescript v1 declaration files
typings/
# Optional npm cache directory
.npm
# Optional eslint cache
.eslintcache
# Optional REPL history
.node_repl_history
# Output of 'npm pack'
*.tgz
# Yarn Integrity file
.yarn-integrity
# dotenv environment variables file
.env
# Build folder
dist/
# .vscode config
.vscode/

3
.npmignore Normal file
View File

@@ -0,0 +1,3 @@
.vscode/
src/
tsconfig.json

3
.vscode/settings.json vendored Normal file
View File

@@ -0,0 +1,3 @@
{
"typescript.tsdk": "node_modules/typescript/lib"
}

View File

@@ -1,6 +1,6 @@
MIT License
Copyright (c) 2018 Rimeto
Copyright (c) 2018-present, Rimeto, LLC.
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal

192
README.md Normal file
View File

@@ -0,0 +1,192 @@
# Optional Chaining for TypeScript
The `ts-oc` library is an implementation of optional chaining with default value support for TypeScript. `ts-oc` helps the developer produce less verbose code while preserving TypeScript typings when traversing deep property structures. This library serves as an interim solution pending JavaScript/TypeScript built-in support for optional chaining in future releases (see: [Related Resources](#related)).
## Install
```bash
npm i --save ts-oc
```
### Requirements
* NodeJS >= 6
* TypeScript >= 2.8
## Example Usage
```typescript
import { oc } from 'ts-oc';
interface I {
a?: string;
b?: {
d?: string;
};
c?: Array<{
u?: {
v?: number;
};
}>;
e?: {
f?: string;
g?: () => string;
};
}
const x: I = {
a: 'hello',
b: {
d: 'world',
},
c: [{ u: { v: -100 } }, { u: { v: 200 } }, {}, { u: { v: -300 } }],
};
// Here are a few examples of deep object traversal using (a) optional chaining vs
// (b) logic expressions. Each of the following pairs are equivalent in
// result. Note how the benefits of optional chaining accrue with
// the depth and complexity of the traversal.
oc(x).a(); // 'hello'
x.a;
oc(x).b.d(); // 'world'
x.b && x.b.d;
oc(x).c[0].u.v(); // -100
x.c && x.c[0] && x.c[0].u && x.c[0].u.v;
oc(x).c[100].u.v(); // undefined
x.c && x.c[100] && x.c[100].u && x.c[100].u.v;
oc(x).c[100].u.v(1234); // 1234
x.c && x.c[100] && x.c[100].u && x.c[100].u.v || 1234;
oc(x).e.f(); // undefined
x.e && x.e.f;
oc(x).e.f('optional default value'); // 'optional default value'
x.e && x.e.f || 'optional default value';
// NOTE: working with function value types can be risky. Additional run-time
// checks to verify that object types are functions before invocation are advised!
oc(x).e.g(() => 'Yo Yo')(); // 'Yo Yo'
(x.e && x.e.g || (() => 'Yo Yo'))();
```
## Problem
When traversing tree-like property structures, the developer often must check for existence of intermediate nodes to avoid run-time exceptions. While TypeScript is helpful in requiring the necessary existence checks at compile-time, the final code is still quite cumbersome. For example, given the interfaces:
```typescript
interface IAddress {
street?: string;
city?: string;
state?: string;
postalCode?: string;
}
interface IHome {
address?: IAddress;
phoneNumber?: string;
}
interface IUser {
home?: IHome;
}
```
Without support for optional chaining built into TypeScript yet, an implementation for a method to extract the home street string from this structure would look like:
```typescript
function getHomeStreet(user: IUser, defaultValue?: string) {
return user.home && user.home.address && user.home.address.street || defaultValue;
}
```
This implementation is tedious to write. Utilities like `lodash`'s `get(...)` can help tighten the implementation, namely:
```typescript
import { get } from 'lodash';
function getHomeStreet(user: IUser, defaultValue?: string) {
return get(user, 'home.address.street', defaultValue);
}
```
However, when using tools like `lodash` the developer loses the benefits of:
* Compile-time validation of the path `home.address.street`
* Compile-time validation of the expected type of the value at `home.address.street`
* Development-time code-completion assistance when manipulating the path `home.address.street` using tools like Visual Studio Code.
## Solution
Using the `ts-oc` utility, `getHomeStreet` can be concisely written as:
```typescript
import { oc } from 'ts-oc';
function getHomeStreet(user: IUser, defaultValue?: string) {
return oc(user).home.address.street(defaultValue);
}
```
Other features of `ts-oc` include:
### Type Preservation
`ts-oc` preserves TypeScript typings through deep tree traversal. For example:
```typescript
// phoneNumberOptional is of type: string | undefined
const phoneNumberOptional = oc(user).home.phoneNumber();
// phoneNumberRequired is of type: string
const phoneNumberRequired = oc(user).home.phoneNumber('+1.555.123.4567');
```
### Array Types
`ts-oc` supports traversal of Array types by index. For example:
```typescript
interface IItem {
name?: string;
}
interface ICollection {
items?: IItem[];
}
function getFirstItemName(collection: ICollection) {
// Return type: string
return oc(collection).items[0].name('No Name Item');
}
```
### Function Types
`ts-oc` supports traversal to function values. For example:
```typescript
interface IThing {
getter?: () => string;
}
const thing: IThing = { ... };
const result = oc(thing).getter(() => 'Default Getter')();
```
### Code-Completion
`ts-oc` enables code-completion assistance in popular IDEs such as Visual Studio Code when writing tree-traversal code.
## <a name="related"></a>Related Resources
* [Optional Chaining for JavaScript (TC39 Proposal)](https://github.com/tc39/proposal-optional-chaining)
## License
`ts-oc` is MIT Licensed.

5226
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

48
package.json Normal file
View File

@@ -0,0 +1,48 @@
{
"name": "ts-oc",
"version": "0.1.0",
"description": "Optional Chaining for TypeScript",
"repository": {
"type": "git",
"url": "git+https://github.com/rimeto/ts-oc.git"
},
"main": "./dist/index.js",
"types": "./dist/index.d.ts",
"scripts": {
"build": "tsc",
"prepublish": "npm run build",
"test": "jest"
},
"license": "MIT",
"devDependencies": {
"@types/jest": "^23.3.1",
"jest": "^23.4.2",
"ts-jest": "^23.1.2",
"typescript": "^2.9.2"
},
"jest": {
"moduleFileExtensions": [
"ts",
"tsx",
"js",
"json"
],
"rootDir": "./src",
"testEnvironment": "node",
"testRegex": "(/__tests__/.*|\\.(test|spec))\\.(ts|tsx|js)$",
"transform": {
".(ts|tsx)": "ts-jest"
}
},
"keywords": [
"existential operator",
"null conditional operator",
"null propagation operator",
"optional chaining",
"safe navigation operator",
"typescript"
],
"engines": {
"node": ">=6"
}
}

View File

@@ -0,0 +1,71 @@
/**
* Copyright (C) 2018-present, Rimeto, LLC.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
import { oc } from '../index';
describe('ts-oc', () => {
it('sanity checks', () => {
interface X {
a: string;
b: { d: string; };
c: number[];
d?: { e: string; };
}
const x = oc<X>({
a: 'hello',
b: {
d: 'world',
},
c: [-100, 200, -300],
});
expect(x.a()).toEqual('hello');
expect(x.b.d()).toEqual('world');
expect(x.c[0]()).toEqual(-100);
expect(x.c[100]()).toBeUndefined();
expect(x.c[100](1234)).toEqual(1234);
expect(x.d.e()).toBeUndefined();
expect(x.d.e('optional default value')).toEqual('optional default value');
expect((x as any).y.z.a.b.c.d.e.f.g.h.i.j.k()).toBeUndefined();
});
it('optional chaining equivalence', () => {
interface X {
a?: string;
b?: {
d?: string;
};
c?: Array<{
u?: {
v?: number;
};
}>;
e?: {
f?: string;
g?: () => string;
};
}
const x: X = {
a: 'hello',
b: {
d: 'world',
},
c: [{ u: { v: -100 } }, { u: { v: 200 } }, {}, { u: { v: -300 } }],
};
expect(oc(x).a()).toEqual(x.a);
expect(oc(x).b.d()).toEqual(x.b && x.b.d);
expect(oc(x).c[0].u.v()).toEqual(x.c && x.c[0] && x.c[0].u && (x as any).c[0].u.v);
expect(oc(x).c[100].u.v()).toEqual(x.c && x.c[100] && x.c[100].u && (x as any).c[100].u.v);
expect(oc(x).c[100].u.v(1234)).toEqual(x.c && x.c[100] && x.c[100].u && (x as any).c[100].u.v || 1234);
expect(oc(x).e.f()).toEqual(x.e && x.e.f);
expect(oc(x).e.f('optional default value')).toEqual(x.e && x.e.f || 'optional default value');
expect(oc(x).e.g(() => 'Yo Yo')()).toEqual((x.e && x.e.g || (() => 'Yo Yo'))());
});
});

129
src/index.ts Normal file
View File

@@ -0,0 +1,129 @@
/**
* Copyright (C) 2018-present, Rimeto, LLC.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
////////////////////////////
//
// Generic Type Definitions
//
////////////////////////////
/**
* A generic type that cannot be `undefined`.
*/
export type Defined<T> = Exclude<T, undefined>;
////////////////////////////
//
// DataAccessor Definitions
//
////////////////////////////
/**
* Interface for the data accessor w/o a default value.
* Note: because no default value exists, we always assume
* the data accessor can return `undefined`.
*/
export type DataAccessorWithoutDefault<T> = () => Defined<T> | undefined;
/**
* Interface for the data accessor w/ default value.
* @param defaultValue
*/
export type DataAccessorWithDefault<T> = (defaultValue: Defined<T>) => Defined<T>;
/**
* Intersection of `DataAccessorWithDefault` and `DataAccessorWithoutDefault`
* to support data access with and without a specified default value.
*/
export type DataAccessor<T> = DataAccessorWithoutDefault<T> & DataAccessorWithDefault<T>;
///////////////////////////
//
// DataWrapper Definitions
//
///////////////////////////
/**
* `ObjectWrapper` gives TypeScript visibility into the properties of
* an `OCType` object at compile-time.
*/
export type ObjectWrapper<T> = { [K in keyof T]-?: OCType<Defined<T[K]>> };
/**
* `ArrayWrapper` gives TypeScript visibility into the `OCType` values of an array
* without exposing Array methods (it is problematic to attempt to invoke methods during
* the course of an optional chain traversal).
*/
export interface ArrayWrapper<T> {
length: OCType<number>;
[K: number]: OCType<T>;
};
/**
* `DataWrapper` selects between `ArrayWrapper`, `ObjectWrapper`, and `DataAccessor` types
* to wrap arrays, objects and primitive types respectively.
*/
export type DataWrapper<T> = T extends any[]
? ArrayWrapper<T[number]>
: T extends object
? ObjectWrapper<T>
: DataAccessor<T>;
/////////////////////////////////////
//
// OCType Definitions
//
////////////////////////////////////
/**
* An object that supports optional chaining
*/
export type OCType<T> = DataWrapper<T> & DataAccessor<T>;
/**
* Proxies access to the passed object to support optional chaining w/ default values.
* To look at a property deep in a tree-like structure, invoke it as a function passing an optional
* default value.
*
* @example
* // Given:
* const x = oc<T>({
* a: 'hello',
* b: { d: 'world' },
* c: [-100, 200, -300],
* });
*
* // Then:
* x.a() === 'hello'
* x.b.d() === 'world'
* x.c[0]() === -100
* x.c[100]() === undefined
* x.c[100](1234) === 1234
* x.c.map((e) => e()) === [-100, 200, -300]
* x.d.e() === undefined
* x.d.e('optional default value') === 'optional default value'
* (x as any).y.z.a.b.c.d.e.f.g.h.i.j.k() === undefined
*/
export function oc<T>(data?: T): OCType<T> {
return new Proxy(
((defaultValue?: Defined<T>) => (data !== undefined ? data : defaultValue)) as OCType<T>,
{
get: (target, key) => {
const obj: any = target();
if ('object' !== typeof obj) {
return oc();
}
return oc(obj[key]);
},
},
);
}

18
tsconfig.json Normal file
View File

@@ -0,0 +1,18 @@
{
"compilerOptions": {
"alwaysStrict": true,
"declaration": true,
"lib": ["es2015"],
"module": "commonjs",
"noEmitOnError": true,
"noImplicitReturns": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"outDir": "dist",
"strict": true,
"strictNullChecks": true,
"target": "es2015",
},
"exclude": ["node_modules"],
"include": ["src"]
}