mirror of
https://github.com/zhigang1992/ts-optchain.git
synced 2026-01-12 09:34:19 +08:00
Initial commit to ts-oc repo (#1)
This commit is contained in:
3
.github/PULL_REQUEST_TEMPLATE.md
vendored
Normal file
3
.github/PULL_REQUEST_TEMPLATE.md
vendored
Normal file
@@ -0,0 +1,3 @@
|
||||
## Description
|
||||
|
||||
## Test Plan
|
||||
64
.gitignore
vendored
Normal file
64
.gitignore
vendored
Normal 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
3
.npmignore
Normal file
@@ -0,0 +1,3 @@
|
||||
.vscode/
|
||||
src/
|
||||
tsconfig.json
|
||||
3
.vscode/settings.json
vendored
Normal file
3
.vscode/settings.json
vendored
Normal file
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"typescript.tsdk": "node_modules/typescript/lib"
|
||||
}
|
||||
2
LICENSE
2
LICENSE
@@ -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
192
README.md
Normal 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
5226
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
48
package.json
Normal file
48
package.json
Normal 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"
|
||||
}
|
||||
}
|
||||
71
src/__tests__/ts-oc-test.ts
Normal file
71
src/__tests__/ts-oc-test.ts
Normal 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
129
src/index.ts
Normal 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
18
tsconfig.json
Normal 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"]
|
||||
}
|
||||
Reference in New Issue
Block a user