Merge branch 'typescript'

typescript
Yanick Champoux 2019-10-24 11:36:30 -04:00
commit 9f66f7e494
28 changed files with 433 additions and 291 deletions

1
index.d.ts vendored Normal file
View File

@ -0,0 +1 @@

17
index.test-d.ts Normal file
View File

@ -0,0 +1,17 @@
import { expectType, expectError } from 'tsd';
import buildInitial from './src/buildInitial';
expectType<{}>(buildInitial());
type MyState = {
foo: {
bar: number
},
baz: string,
}
expectType<MyState>(buildInitial<MyState>());
expectError( buildInitial<MyState>({ foo: { bar: "potato" } }) );

9
jest.config.js Normal file
View File

@ -0,0 +1,9 @@
module.exports = {
"roots": [
"./src"
],
"transform": {
"^.+\\.ts$": "ts-jest",
"^.+\\.js$": "babel-jest",
},
}

View File

@ -7,8 +7,13 @@
"@babel/cli": "^7.6.4",
"@babel/core": "^7.6.4",
"@babel/preset-env": "^7.6.3",
"@types/jest": "^24.0.19",
"@types/lodash": "^4.14.144",
"babel-jest": "^24.9.0",
"jest": "^24.9.0",
"ts-jest": "^24.1.0",
"tsd": "^0.10.0",
"typescript": "^3.6.4",
"updeep": "^1.2.0"
},
"license": "MIT",
@ -20,16 +25,18 @@
"test": "jest"
},
"version": "0.1.0",
"repository": {
"type": "git",
"url": "git+https://github.com/yanick/updux.git"
},
"keywords": [
"redux", "updeep"
],
"author": "Yanick Champoux <yanick@babyl.ca> (http://techblog.babyl.ca)",
"bugs": {
"url": "https://github.com/yanick/updux/issues"
},
"homepage": "https://github.com/yanick/updux#readme"
"repository": {
"type": "git",
"url": "git+https://github.com/yanick/updux.git"
},
"keywords": [
"redux",
"updeep"
],
"author": "Yanick Champoux <yanick@babyl.ca> (http://techblog.babyl.ca)",
"bugs": {
"url": "https://github.com/yanick/updux/issues"
},
"homepage": "https://github.com/yanick/updux#readme",
"types": "./index.d.ts"
}

View File

@ -1,38 +0,0 @@
import updux from '.';
import u from 'updeep';
test( 'actions defined in effects and mutations, multi-level', () => {
const { actions } = updux({
effects: {
foo: api => next => action => { },
},
mutations: { bar: () => () => null },
subduxes: {
mysub: {
effects: { baz: api => next => action => { }, },
mutations: { quux: () => () => null },
actions: {
foo: (limit) => ({limit}),
},
},
myothersub: {
effects: {
foo: () => () => () => {},
},
}
},
});
const types = Object.keys(actions);
types.sort();
expect( types).toEqual([ 'bar', 'baz', 'foo', 'quux', ]);
expect( actions.bar() ).toEqual({ type: 'bar' });
expect( actions.bar('xxx') ).toEqual({ type: 'bar', payload: 'xxx' });
expect( actions.bar(undefined,'yyy') ).toEqual({ type: 'bar',meta: 'yyy' });
expect(actions.foo(12)).toEqual({type: 'foo', payload: { limit: 12 }});
});

38
src/actions.test.ts Normal file
View File

@ -0,0 +1,38 @@
import updux from '.';
import u from 'updeep';
const noopEffect = () => () => () => {};
test('actions defined in effects and mutations, multi-level', () => {
const {actions} = updux({
effects: {
foo: noopEffect,
},
mutations: {bar: () => () => null},
subduxes: {
mysub: {
effects: {baz: noopEffect },
mutations: {quux: () => () => null},
actions: {
foo: (limit:number) => ({limit}),
},
},
myothersub: {
effects: {
foo: noopEffect,
},
},
},
});
const types = Object.keys(actions);
types.sort();
expect(types).toEqual(['bar', 'baz', 'foo', 'quux']);
expect(actions.bar()).toEqual({type: 'bar'});
expect(actions.bar('xxx')).toEqual({type: 'bar', payload: 'xxx'});
expect(actions.bar(undefined, 'yyy')).toEqual({type: 'bar', meta: 'yyy'});
expect(actions.foo(12)).toEqual({type: 'foo', payload: {limit: 12}});
});

View File

@ -1,40 +0,0 @@
import fp from 'lodash/fp';
function actionFor(type) {
const creator = ( (payload = undefined, meta = undefined) =>
fp.pickBy(v => v !== undefined)({type, payload, meta})
);
creator._genericAction = true;
return creator;
}
export default function buildActions(
creators = {},
mutations = {},
effects = {},
subActions = [],
) {
// priority => generics => generic subs => craft subs => creators
const [ crafted, generic ] = fp.partition(
([type,f]) => !f._genericAction
)( fp.flatten( subActions.map( x => Object.entries(x) ) ).filter(
([_,f]) => f
) )
const actions = [
...([ ...Object.keys(mutations), ...Object.keys(effects) ]
.map( type => [ type, actionFor(type) ] )),
...generic,
...crafted,
...Object.entries(creators).map(
([type, payload]) => [type, (...args) => ({ type, payload: payload(...args) })]
),
];
return fp.fromPairs(actions);
}

46
src/buildActions/index.ts Normal file
View File

@ -0,0 +1,46 @@
import fp from 'lodash/fp';
import { Action, ActionPayloadGenerator, Dictionary } from '../types';
interface ActionCreator {
( ...args: any[] ): Action;
_genericAction?: boolean
}
function actionFor(type:string) {
const creator : ActionCreator = ( (payload = undefined, meta = undefined) =>
fp.pickBy(v => v !== undefined)({type, payload, meta}) as Action
);
creator._genericAction = true;
return creator;
}
type ActionPair = [ string, ActionCreator ];
function buildActions(
generators : Dictionary<ActionPayloadGenerator> = {},
actionNames: string[] = [],
subActions : ActionPair[] = [],
):Dictionary<ActionCreator> {
// priority => generics => generic subs => craft subs => creators
const [ crafted, generic ] = fp.partition(
([type,f]) => !f._genericAction
)( subActions );
const actions = [
...(actionNames.map( type => [ type, actionFor(type) ] )),
...generic,
...crafted,
...Object.entries(generators).map(
([type, payload]: [ string, Function ]) => [type, (...args: any) => ({ type, payload: payload(...args) })]
),
];
return fp.fromPairs(actions);
}
export default buildActions;

View File

@ -1,17 +0,0 @@
import { createStore as reduxCreateStore, applyMiddleware } from 'redux';
export default function buildCreateStore( reducer, initial, middleware,
actions ) {
return () => {
const store = reduxCreateStore( reducer, initial,
applyMiddleware( middleware)
);
for ( let a in actions ) {
store.dispatch[a] = (...args) => {
store.dispatch(actions[a](...args))
};
}
return store;
}
};

View File

@ -0,0 +1,31 @@
import {
createStore as reduxCreateStore,
applyMiddleware,
Middleware,
Reducer,
} from 'redux';
import { ActionCreator, Dictionary } from '../types';
function buildCreateStore<S>(
reducer: Reducer<S>,
initial: S,
middleware: Middleware,
actions: Dictionary<ActionCreator>,
) {
return () => {
const store = reduxCreateStore(
reducer,
initial,
applyMiddleware(middleware),
);
for (let a in actions) {
( store.dispatch as any)[a] = (...args: any[]) => {
store.dispatch(actions[a](...args));
};
}
return store;
};
}
export default buildCreateStore;

View File

@ -1,8 +0,0 @@
import fp from 'lodash/fp';
export default function buildInitial(
initial= {},
subduxes = {},
) {
return fp.isPlainObject(initial) ? fp.mergeAll([subduxes, initial]) : initial;
}

13
src/buildInitial/index.ts Normal file
View File

@ -0,0 +1,13 @@
import fp from 'lodash/fp';
import { Dictionary } from '../types';
function buildInitial<S extends number|string|boolean>( initial: S, subduxes?: Dictionary<undefined> ): S;
function buildInitial<S extends object>( initial?: Partial<S>, subduxes?: Partial<S> ): S extends object ? S : never;
function buildInitial(
initial : any = {},
subduxes : any = {} ,
) {
return fp.isPlainObject(initial) ? fp.mergeAll([subduxes, initial]) : initial;
}
export default buildInitial;

View File

@ -1,30 +0,0 @@
import fp from 'lodash/fp';
const MiddlewareFor = (type,mw) => api => next => action => {
if (type !== '*' && action.type !== type) return next(action);
return mw(api)(next)(action);
};
export default function buildMiddleware(
effects = {},
actions = {},
subduxes = {},
) {
return api => {
for (let type in actions) {
api.dispatch[type] = (...args) => api.dispatch(actions[type](...args));
}
return original_next => {
return [
...fp.toPairs(effects).map(([type, effect]) =>
MiddlewareFor(type,effect)
),
...fp.map('middleware', subduxes),
]
.filter(x => x)
.reduceRight((next, mw) => mw(api)(next), original_next);
};
};
}

View File

@ -0,0 +1,38 @@
import fp from 'lodash/fp';
import { Middleware, MiddlewareAPI, Dispatch } from 'redux';
import { Dictionary, ActionCreator, Action, UpduxDispatch } from '../types';
const MiddlewareFor = (type: any, mw: Middleware ): Middleware => api => next => action => {
if (type !== '*' && action.type !== type) return next(action);
return mw(api)(next)(action);
};
type Next = (action: Action) => any;
function buildMiddleware<S=any>(
effects : Dictionary<Middleware<{},S,UpduxDispatch>>= {},
actions : Dictionary<ActionCreator>= {},
subMiddlewares :Middleware<{},S,UpduxDispatch>[] = [],
): Middleware<{},S,UpduxDispatch>
{
return (api: MiddlewareAPI<UpduxDispatch,S>) => {
for (let type in actions) {
api.dispatch[type] = (...args:any[]) => api.dispatch(((actions as any)[type] as any)(...args));
}
return (original_next: Next)=> {
return [
...fp.toPairs(effects).map(([type, effect]) =>
MiddlewareFor(type,effect as Middleware)
),
...subMiddlewares
]
.filter(x => x)
.reduceRight((next, mw) => mw(api)(next), original_next);
};
};
}
export default buildMiddleware;

View File

@ -1,53 +0,0 @@
import fp from 'lodash/fp';
import u from 'updeep';
const composeMutations = (mutations) =>
mutations.reduce( (m1,m2) =>
(payload=null,action={}) => state => m2(payload,action)(
m1(payload,action)(state) ));
export default function buildMutations(mutations = {}, subduxes= {}) {
// we have to differentiate the subduxes with '*' than those
// without, as the root '*' is not the same as any sub-'*'
const actions = fp.uniq( Object.keys(mutations).concat(
...Object.values( subduxes ).map( ({mutations = {}}) => Object.keys(mutations) )
) );
let mergedMutations = {};
let [ globby, nonGlobby ] = fp.partition(
([_,{mutations={}}]) => mutations['*'],
Object.entries(subduxes)
);
globby =
fp.flow([
fp.fromPairs,
fp.mapValues(
({reducer}) => (_,action={}) => state =>
reducer(state,action) ),
])(globby);
const globbyMutation = (payload,action) => u(
fp.mapValues( (mut) => mut(payload,action) )(globby)
);
actions.forEach( action => {
mergedMutations[action] = [ globbyMutation ]
});
nonGlobby.forEach( ([slice, {mutations={},reducer={}}]) => {
Object.entries(mutations).forEach(([type,mutation]) => {
const localized = (payload=null,action={}) => u.updateIn( slice )( (mutation)(payload,action) );
mergedMutations[type].push(localized);
})
});
Object.entries(mutations).forEach(([type,mutation]) => {
mergedMutations[type].push(mutation);
});
return fp.mapValues( composeMutations )(mergedMutations);
}

View File

@ -0,0 +1,66 @@
import fp from 'lodash/fp';
import u from 'updeep';
import {Mutation, Action, Dictionary} from '../types';
const composeMutations = (mutations: Mutation[]) =>
mutations.reduce((m1, m2) => (payload: any = null, action: Action) => state =>
m2(payload, action)(m1(payload, action)(state)),
);
type SubMutations = {
[ slice: string ]: Dictionary<Mutation>
}
function buildMutations(
mutations :Dictionary<Mutation> = {},
subduxes = {}
) {
// we have to differentiate the subduxes with '*' than those
// without, as the root '*' is not the same as any sub-'*'
const actions = fp.uniq(
Object.keys(mutations).concat(
...Object.values(subduxes).map(({mutations = {}}:any) =>
Object.keys(mutations),
),
),
);
let mergedMutations :Dictionary<Mutation[]> = {};
let [globby, nonGlobby] = fp.partition(
([_, {mutations = {}}]:any) => mutations['*'],
Object.entries(subduxes),
);
globby = fp.flow([
fp.fromPairs,
fp.mapValues(({reducer}) => (_:any, action :Action) => ( state: any ) =>
reducer(state, action),
),
])(globby);
const globbyMutation = (payload:any, action:Action) =>
u(fp.mapValues((mut:any) => mut(payload, action))(globby));
actions.forEach(action => {
mergedMutations[action] = [globbyMutation];
});
nonGlobby.forEach(([slice, {mutations = {}, reducer = {}}]:any[]) => {
Object.entries(mutations).forEach(([type, mutation]) => {
const localized = (payload = null, action :Action) =>
u.updateIn(slice)((mutation as Mutation)(payload, action));
mergedMutations[type].push(localized);
});
});
Object.entries(mutations).forEach(([type, mutation]) => {
mergedMutations[type].push(mutation);
});
return fp.mapValues(composeMutations)(mergedMutations);
}
export default buildMutations;

View File

@ -1,15 +0,0 @@
import fp from 'lodash/fp';
export default function buildUpreducer(initial, mutations) {
return (action = {}) => (state) => {
if (state === null) state = initial;
const a =
mutations[(action).type] ||
mutations['*'];
if(!a) return state;
return a((action).payload, action)(state);
};
}

View File

@ -0,0 +1,19 @@
import fp from 'lodash/fp';
import { Dictionary, Mutation, Action, Upreducer } from '../types';
function buildUpreducer<S>(initial: S, mutations: Dictionary<Mutation<S>> ): Upreducer<S> {
return (action :Action) => (state: S) => {
if (state === null) state = initial;
const a =
mutations[action.type] ||
mutations['*'];
if(!a) return state;
return a(action.payload, action)(state);
};
}
export default buildUpreducer;

View File

@ -3,6 +3,8 @@ import u from 'updeep';
import Updux from './updux';
export default function updux(config) {
import { UpduxConfig } from './types';
export default function updux(config: UpduxConfig) {
return new Updux(config);
}

View File

@ -7,7 +7,7 @@ test( 'simple effect', () => {
const store = updux({
effects: {
foo: api => next => action => {
foo: (api:any) => (next:any) => (action:any) => {
tracer();
next(action);
},
@ -30,7 +30,7 @@ test( 'effect and sub-effect', () => {
const tracer = jest.fn();
const tracerEffect = signature => api => next => action => {
const tracerEffect = ( signature: string ) => ( api:any ) => (next:any) => ( action: any ) => {
tracer(signature);
next(action);
};
@ -83,7 +83,7 @@ test( '"*" effect', () => {
test( 'async effect', async () => {
function timeout(ms) {
function timeout(ms:number) {
return new Promise(resolve => setTimeout(resolve, ms));
}

View File

@ -1,13 +1,13 @@
import updux from '.';
import u from 'updeep';
const tracer = chr => u({ tracer: s => (s||'') + chr });
const tracer = (chr:string) => u({ tracer: (s='') => s + chr });
test( 'mutations, simple', () => {
const dux = updux({
mutations: {
foo: () => tracer('a'),
'*': (p,a) => tracer('b'),
'*': () => tracer('b'),
},
});
@ -28,14 +28,14 @@ test( 'with subduxes', () => {
const dux = updux({
mutations: {
foo: () => tracer('a'),
'*': (dummy,a) => tracer('b'),
bar: () => ({bar}) => ({ bar, tracer: bar.tracer })
'*': () => tracer('b'),
bar: () => ({bar}:any) => ({ bar, tracer: bar.tracer })
},
subduxes: {
bar: updux({
mutations: {
foo: () => tracer('d'),
'*': (dummy,a) => tracer('e'),
'*': () => tracer('e'),
},
}),
},

View File

@ -5,7 +5,7 @@ test('actions from mutations', () => {
actions: {foo, bar},
} = updux({
mutations: {
foo: () => x => x,
foo: () => (x:any) => x,
},
});
@ -24,11 +24,11 @@ test('reducer', () => {
const {actions, reducer} = updux({
initial: {counter: 1},
mutations: {
inc: () => ({counter}) => ({counter: counter + 1}),
inc: () => ({counter}:{counter:number}) => ({counter: counter + 1}),
},
});
let state = reducer(null, {});
let state = reducer(null, {type:'noop'});
expect(state).toEqual({counter: 1});
@ -41,16 +41,16 @@ test( 'sub reducers', () => {
const foo = updux({
initial: 1,
mutations: {
doFoo: () => (x) => x + 1,
doAll: () => x => x + 10,
doFoo: () => (x:number) => x + 1,
doAll: () => (x:number) => x + 10,
},
});
const bar = updux({
initial: 'a',
mutations: {
doBar: () => x => x + 'a',
doAll: () => x => x + 'b',
doBar: () => (x:string) => x + 'a',
doAll: () => (x:string) => x + 'b',
}
});
@ -64,7 +64,7 @@ test( 'sub reducers', () => {
expect(Object.keys(actions)).toHaveLength(3);
let state = reducer(null,{});
let state = reducer(null,{type:'noop'});
expect(state).toEqual({ foo: 1, bar: 'a' });
@ -92,7 +92,7 @@ test('precedence between root and sub-reducers', () => {
foo: { bar: 4 },
},
mutations: {
inc: () => state => {
inc: () => (state:any) => {
return {
...state,
surprise: state.foo.bar
@ -106,7 +106,7 @@ test('precedence between root and sub-reducers', () => {
quux: 3,
},
mutations: {
inc: () => state => ({...state, bar: state.bar + 1 })
inc: () => (state:any) => ({...state, bar: state.bar + 1 })
},
}),
}
@ -122,7 +122,7 @@ test('precedence between root and sub-reducers', () => {
});
function timeout(ms) {
function timeout(ms:number) {
return new Promise(resolve => setTimeout(resolve, ms));
}
@ -133,8 +133,8 @@ test( 'middleware', async () => {
} = updux({
initial: "",
mutations: {
inc: (addition) => state => state + addition,
doEeet: () => state => {
inc: (addition:number) => (state:number) => state + addition,
doEeet: () => (state:number) => {
return state + 'Z';
},
},

29
src/types.ts Normal file
View File

@ -0,0 +1,29 @@
import { Dispatch, Middleware } from 'redux';
export type Action = {
type: string,
payload?: any,
meta?: any,
}
export type Dictionary<T> = { [key: string]: T };
export type Mutation<S=any> = (payload: any, action: Action) => (state: S) => S ;
export type ActionPayloadGenerator = (...args:any[]) => any;
export type ActionCreator = (...args: any[] ) => Action;
export type UpduxDispatch = Dispatch & Dictionary<ActionCreator>;
export type UpduxConfig<S=any> = Partial<{
initial: S,
subduxes: {},
actions: {
[ type: string ]: ActionPayloadGenerator
},
mutations: any,
effects: Dictionary<Middleware<{},S,UpduxDispatch>>,
}>;
export type Upreducer<S=any> = (action:Action) => (state:S) => S;

View File

@ -1,53 +0,0 @@
import fp from 'lodash/fp';
import buildActions from './buildActions';
import buildInitial from './buildInitial';
import buildMutations from './buildMutations';
import buildCreateStore from './buildCreateStore';
import buildMiddleware from './buildMiddleware';
import buildUpreducer from './buildUpreducer';
export class Updux {
constructor(config) {
this.subduxes = fp.mapValues(
value => fp.isPlainObject(value) ? new Updux(value ) : value )(fp.getOr({},'subduxes',config)
);
this.actions = buildActions(
config.actions,
config.mutations,
config.effects,
Object.values( this.subduxes ).map( ({actions}) => actions ),
)
this.initial = buildInitial(
config.initial, fp.mapValues( ({initial}) => initial )(this.subduxes)
);
this.mutations = buildMutations(
config.mutations, this.subduxes
);
this.upreducer = buildUpreducer(
this.initial, this.mutations
);
this.reducer = (state,action) => {
return this.upreducer(action)(state);
}
this.middleware = buildMiddleware(
config.effects,
this.actions,
config.subduxes,
);
this.createStore = buildCreateStore(this.reducer,this.initial,this.middleware,this.actions);
}
}
export default Updux;

77
src/updux.ts Normal file
View File

@ -0,0 +1,77 @@
import fp from 'lodash/fp';
import buildActions from './buildActions';
import buildInitial from './buildInitial';
import buildMutations from './buildMutations';
import buildCreateStore from './buildCreateStore';
import buildMiddleware from './buildMiddleware';
import buildUpreducer from './buildUpreducer';
import { UpduxConfig, Dictionary, Action, ActionCreator, Mutation, Upreducer, UpduxDispatch } from './types';
import { Middleware, Store } from 'redux';
type StoreWithDispatchActions<S=any,Actions={ [action: string]: (...args:any) => Action }> = Store<S> & {
dispatch: { [ type in keyof Actions ]: (...args:any) => void }
};
export class Updux<S=any> {
subduxes: Dictionary<Updux>;
actions: Dictionary<ActionCreator>
initial: S;
mutations: Dictionary<Mutation>;
upreducer: Upreducer<S>;
reducer: (state:S|undefined,action:Action) => S;
middleware: Middleware<{},S,UpduxDispatch>;
createStore: () => StoreWithDispatchActions<S>;
constructor(config: UpduxConfig) {
this.subduxes = fp.mapValues(
(value:UpduxConfig|Updux) => fp.isPlainObject(value) ? new Updux(value) : value )(fp.getOr({},'subduxes',config)
) as Dictionary<Updux>;
this.actions = buildActions(
config.actions,
[ ...Object.keys(config.mutations||{}), ...Object.keys(config.effects||{} ) ],
fp.flatten( Object.values( this.subduxes ).map( ({actions}:Updux) => Object.entries(actions) ) ),
)
this.initial = buildInitial<any>(
config.initial, fp.mapValues( ({initial}) => initial )(this.subduxes)
);
this.mutations = buildMutations(
config.mutations, this.subduxes
);
this.upreducer = buildUpreducer(
this.initial, this.mutations
);
this.reducer = (state,action) => {
return this.upreducer(action)(state as S);
}
this.middleware = buildMiddleware(
config.effects,
this.actions,
Object.values(this.subduxes).map( sd => sd.middleware )
);
const actions = this.actions;
this.createStore = buildCreateStore<S>(this.reducer,this.initial,this.middleware as Middleware,this.actions) as
() => StoreWithDispatchActions< S, typeof actions >;
}
}
export default Updux;

View File

@ -1,11 +1,14 @@
{
"include": [
"./src/**/*"
],
"compilerOptions": {
/* Basic Options */
"incremental": true, /* Enable incremental compilation */
"target": "ES2019", /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019' or 'ESNEXT'. */
"module": "commonjs", /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', or 'ESNext'. */
"lib": [ "dom", "es2019" ], /* Specify library files to be included in the compilation. */
// "allowJs": true, /* Allow javascript files to be compiled. */
"allowJs": false, /* Allow javascript files to be compiled. */
// "checkJs": true, /* Report errors in .js files. */
// "jsx": "preserve", /* Specify JSX code generation: 'preserve', 'react-native', or 'react'. */
"declaration": true, /* Generates corresponding '.d.ts' file. */
@ -14,7 +17,7 @@
// "outFile": "./", /* Concatenate and emit output to single file. */
"outDir": "./dist", /* Redirect output structure to the directory. */
"rootDir": "./src", /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */
// "composite": true, /* Enable project compilation */
//"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. */
@ -24,7 +27,7 @@
/* Strict Type-Checking Options */
"strict": true, /* Enable all strict type-checking options. */
"noImplicitAny": false, /* Raise error on expressions and declarations with an implied 'any' type. */
"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. */