Skip to content

Commit

Permalink
Add typescript definitions
Browse files Browse the repository at this point in the history
resolves #24

Tested and linted typescript definitions for TS ⩾ 3.5.
Run tests with `npm run test:ts` which uses dtslint in the background.

The root of typings are in types/patchinko, a requirement from dtslint.
The most important tests are located in __tests__/explicit and
__tests__/overlaoded.

One big feature of those typings is to strictly enforce patchinko
rules. As such, every do and don't from the readme has been carefuly
tested and implemented.

One limitation of those types however, is that the returned type of a
P(target, ...) call will always be typeof target. Although it could be
 possible, in theory, to find the resulting type from operations such
as deletions ore scoped replacements, I have found this incredibly
difficult and wasn't certain the compiler could follow this level of
complexity and computational burden. Therefore, I thought it would be
an acceptable compromise to return the target type, given the vast
majority of use-cases where one want such outcome.

On the test side, dtslint does the following:

- Lint the sources according to DefinitelyTyped guidelines;
- Run any test file and check for $ExpectType and $ExpectError
  instructions, report compilation errors;
- Run one batch of test for each TypeScript supported version.
  • Loading branch information
jsamr committed Feb 17, 2020
1 parent cec1375 commit 4c8249a
Show file tree
Hide file tree
Showing 14 changed files with 1,295 additions and 102 deletions.
841 changes: 740 additions & 101 deletions package-lock.json

Large diffs are not rendered by default.

9 changes: 8 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,15 +4,22 @@
"description": "A concise tool for declarative object manipulation",
"main": "index.js",
"module": "index.mjs",
"types": "types/patchinko/index.d.ts",
"repository": "[email protected]:barneycarroll/patchinko.git",
"author": "Barney Carroll <[email protected]>",
"license": "MIT",
"devDependencies": {
"conditional-type-checks": "^1.0.5",
"dts-gen": "^0.5.7",
"eslint": "^5.15.1",
"ospec": "3.0.1"
},
"scripts": {
"test": "ospec && eslint ."
"test": "npm run test:lint . && npm run test:ts",
"test:lint": "ospec && eslint",
"test:ts": "dtslint types/patchinko"
},
"dependencies": {
"dtslint": "^2.0.5"
}
}
84 changes: 84 additions & 0 deletions types/patchinko/__tests__/explicit.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
import { P, PS, D, S } from "patchinko";

// Integration tests

interface Bar {
bish: string;
}

interface X {
foo?: {
bar: Bar;
oop?: string
};
z: string;
}

const x: X = {
foo: { bar: { bish: "bish" } },
z: 'zoo'
};

// Correct
// $ExpectType X
P(x, { foo: D });

// Correct
// $ExpectType X
P(x, { foo: PS({ bar: D }) });

// Correct
// $ExpectType X
P(x, { foo: PS({ oop: D, bar: { bish: "bash" } }) });

// Correct, not all properties are required as a PS patch argument.
// $ExpectType X
P(x, { foo: PS({ oop: D }) });

// Incorrect, we are adding an unknown property to X['foo'].
// $ExpectError
P(x, { foo: PS({ tada: "" }) });

// Correct
// $ExpectType X
P(x, { foo: PS({ bar: PS({ bish: "bash" }) }) });

// Also correct - but `bar` will not be patched - instead it will be replaced.
// $ExpectType X
P(x, { foo: PS({ bar: { bish: "bash" } }) });

// Correct usage of S
// $ExpectType X
P(x, { foo: S((old: X['foo']) => ({ bar: { bish: (old && old.bar.bish || '') + 'bash' } }))});

// Correct usage of S nested in a PS call
// $ExpectType X
P(x, { foo: PS({ bar: S(() => ({ bish: 'oops' })) }) });

// Incorrect - we can't patch `bar` because its container - `foo` is a wholesale replacement:
// $ExpectError
P(x, PS({ foo: { bar: PS({ bish: "bash" }) } }));

// Incorrect - primitive values cannot be patched
// $ExpectError
P(x, { foo: PS({ bar: PS({ bish: PS("bash") }) }) });

// Incorrect - wrapping is only necessary for child structures - patch arguments will always patch, not replace
// $ExpectError
P(x, PS({ foo: PS({ bar: PS({ bish: "bash" }) }) }));

// Incorrect, D must not be called
// $ExpectError
P(x, { foo: D() });

// Incorrect usage of D nested in a S patch
// $ExpectError
P(x, { foo: S(() => ({ bar: D })) });

// Correct usage of PS second signature
// $ExpectType X
P(x, { foo: PS({}, { bar: { bish: "bash" }}) });

// Incorrect, we are adding an unknown property to X['foo'].
// $ExpectError
P(x, { foo: PS({}, { tada: 'bah' }) });
46 changes: 46 additions & 0 deletions types/patchinko/__tests__/from-patch-request.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import { assert, IsExact, IsNever } from "conditional-type-checks";
import { PSInstruction, FromPatchRequest, DInstruction, SInstruction } from 'patchinko';
import { Target, InnerTarget } from "./targets";

interface DeepType {
a: {
b: Target
};
}

interface DeeplyNestedInstructions {
a: PSInstruction<{
b: PSInstruction<Target>;
}>;
}

interface BaseType {
a: Target;
}

interface WithDeleteInstructions {
a: DInstruction<Target>;
}

interface WithScopeInstructions {
a: SInstruction<Target>;
}

interface WithOrphanedInstructions {
t: {
// Cannot patch `c` because its container `t` is a substitution.
c: PSInstruction<InnerTarget>
};
}

// WithOrphanedInstruction should not be candidate for extraction.
assert<IsNever<FromPatchRequest<WithOrphanedInstructions>>>(true);

// Extracting from WithDeleteInstructions should result in BaseType.
assert<IsExact<BaseType, FromPatchRequest<WithDeleteInstructions>>>(true);

// Extracting from WithScopeInstructions should result in BaseType.
assert<IsExact<BaseType, FromPatchRequest<WithScopeInstructions>>>(true);

// Extracting from DeeplyNestedInstructions should result in DeepType.
assert<IsExact<DeepType, FromPatchRequest<DeeplyNestedInstructions>>>(true);
26 changes: 26 additions & 0 deletions types/patchinko/__tests__/instances.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import { P, PS } from 'patchinko';
import { assert, IsExact } from 'conditional-type-checks';
import { Target } from './targets';

// Handling of instances from arbitrary classes

interface WithSet {
a: Set<Target>;
}

interface WithArray {
b: Target[];
}

declare const withSet: WithSet;
declare const withArray: WithArray;

const test1 = P(withSet, { a: new Set<Target>() });
assert<IsExact<WithSet, typeof test1>>(true);

// Monkey patching
const test2 = P(withSet, { a: PS({ clear: () => {} })});
assert<IsExact<WithSet, typeof test2>>(true);

const test3 = P(withArray, { b: [] });
assert<IsExact<WithArray, typeof test3>>(true);
13 changes: 13 additions & 0 deletions types/patchinko/__tests__/never-ascend.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import { NeverAscend } from "patchinko";
import { assert, IsNever } from "conditional-type-checks";

assert<IsNever<NeverAscend<{}>>>(false);
assert<IsNever<NeverAscend<{ a: never }>>>(true);
assert<IsNever<NeverAscend<{ a: never; b: string }>>>(true);
assert<IsNever<NeverAscend<{ a: number; b: string }>>>(false);
assert<IsNever<NeverAscend<{ a: any }>>>(false);
assert<IsNever<NeverAscend<{ a: unknown }>>>(false);
assert<IsNever<NeverAscend<{ a: undefined }>>>(false);
assert<IsNever<NeverAscend<{ a: string }>>>(false);
assert<IsNever<NeverAscend<{ a?: string }>>>(false);
assert<IsNever<NeverAscend<{ a?: never }>>>(true);
72 changes: 72 additions & 0 deletions types/patchinko/__tests__/overloaded.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
import { immutable as O, D, WholesomeReplacement, DSymbol, Overloaded, DInstruction, SPatchFunction } from "patchinko";

// Integration tests

interface Bar {
bish: string;
}

interface X {
foo?: {
bar: Bar;
oop?: string
};
z: string;
}

const x: X = {
foo: { bar: { bish: "bish" } },
z: 'zoo'
};

// Correct
// $ExpectType X
O(x, { foo: O });

// Correct
// $ExpectType X
O(x, { foo: O({ bar: O }) });

// Correct
// $ExpectType X
O(x, { foo: O({ oop: O, bar: { bish: "bash" } }) });

// Correct, not all properties are required as a O patch argument.
// $ExpectType X
O(x, { foo: O({ oop: O }) });

// Correct
// $ExpectType X
O(x, { foo: O({ bar: O({ bish: "bash" }) }) });

// Also correct - but `bar` will not be patched - instead it will be replaced.
// $ExpectType X
O(x, { foo: O({ bar: { bish: "bash" } }) });

// Correct usage of O
// $ExpectType X
O(x, { foo: O((old: X['foo']) => ({ bar: { bish: (old && old.bar.bish || '') + 'bash' } }))});

// Correct usage of O nested in a O call
// $ExpectType X
O(x, { foo: O({ bar: O(() => ({ bish: 'oops' })) }) });

// Incorrect - we can't patch `bar` because its container - `foo` is a wholesale replacement:
// $ExpectError
O(x, O({ foo: { bar: O({ bish: "bash" }) } }));

// Incorrect - primitive values cannot be patched
// $ExpectError
O(x, { foo: O({ bar: O({ bish: O("bash") }) }) });

// Incorrect - wrapping is only necessary for child structures - patch arguments will always patch, not replace
// $ExpectError
O(x, O({ foo: O({ bar: O({ bish: "bash" }) }) }));

// Incorrect, O cannot be called with no argument
// $ExpectError
O(x, { foo: O() });

// Incorrect usage of O nested in a O patch
// $ExpectError
O(x, { foo: O(() => ({ bar: O })) });
44 changes: 44 additions & 0 deletions types/patchinko/__tests__/patch-request-of.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import { assert, IsNever, NotHas, Has } from "conditional-type-checks";
import { PatchRequestOf, PSInstruction, DInstruction, P } from 'patchinko';
import { Target, InnerTarget } from "./targets";

interface Original {
t: {
c: InnerTarget
};
}

interface RequestWithOrphanedInstruction {
t: {
// Cannot patch `c` because its container `t` is a substitution.
c: PSInstruction<InnerTarget>
};
}

// RequestWithOrphanedInstruction should not extend patch request.
assert<NotHas<RequestWithOrphanedInstruction, PatchRequestOf<Original>>>(true);

// A p patch request of InnerTarget should contain InnerTarget.
assert<Has<InnerTarget, PatchRequestOf<InnerTarget>>>(true);

// A p patch request of Target should contain Target.
assert<Has<Target, PatchRequestOf<Target>>>(true);

// Patch requests of primitives should result in never.
assert<IsNever<PatchRequestOf<string>>>(true);
assert<IsNever<PatchRequestOf<number>>>(true);
assert<IsNever<PatchRequestOf<symbol>>>(true);
assert<IsNever<PatchRequestOf<null>>>(true);
assert<IsNever<PatchRequestOf<undefined>>>(true);
assert<IsNever<PatchRequestOf<bigint>>>(true);

interface OptionalTarget {
target?: Target;
}

interface DeleteRequest {
target: DInstruction<Target>;
}

// DeleteRequest should extend patch request of OptionalTarget.
assert<Has<DeleteRequest, PatchRequestOf<OptionalTarget>>>(true);
27 changes: 27 additions & 0 deletions types/patchinko/__tests__/targets.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import { PSInstruction } from "patchinko";

export interface InnerTarget {
a: string;
}

export interface Target {
a: string;
b: number;
c: InnerTarget;
}

export interface DeeplyNestedTarget {
a: {
b: {
c: Target
}
};
}

export interface DeeplyNestedRequest {
a: PSInstruction<{
b: PSInstruction<{
c: PSInstruction<Target>
}>
}>;
}
19 changes: 19 additions & 0 deletions types/patchinko/__tests__/to-patch-request.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import { assert, IsExact, IsNever } from "conditional-type-checks";
import { ToPatchRequest } from 'patchinko';
import { Target, InnerTarget } from "./targets";

type T = ToPatchRequest<InnerTarget>;

// InnerTarget should be coercible to patch requests.
assert<IsExact<ToPatchRequest<InnerTarget>, InnerTarget>>(true);

// Target should be coercible to patch requests.
assert<IsExact<ToPatchRequest<Target>, Target>>(true);

// Primitives cannot be coerced to patch requests.
assert<IsNever<ToPatchRequest<string>>>(true);
assert<IsNever<ToPatchRequest<number>>>(true);
assert<IsNever<ToPatchRequest<symbol>>>(true);
assert<IsNever<ToPatchRequest<null>>>(true);
assert<IsNever<ToPatchRequest<undefined>>>(true);
assert<IsNever<ToPatchRequest<bigint>>>(true);
16 changes: 16 additions & 0 deletions types/patchinko/__tests__/wholesome-replacement.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import { assert, IsNever, IsExact } from "conditional-type-checks";
import { PSInstruction, WholesomeReplacement, DInstruction, Overloaded } from "patchinko";
import { Target, InnerTarget } from "./targets";

interface NestedPSInstruction {
b: PSInstruction<Target>;
}

assert<IsExact<WholesomeReplacement<string>, string>>(true);
assert<IsExact<WholesomeReplacement<InnerTarget>, InnerTarget>>(true);
assert<IsExact<WholesomeReplacement<Target>, Target>>(true);
assert<IsNever<WholesomeReplacement<PSInstruction<Target>>>>(true);
assert<IsNever<WholesomeReplacement<NestedPSInstruction>>>(true);
assert<IsNever<WholesomeReplacement<{ a: NestedPSInstruction }>>>(true);
assert<IsNever<WholesomeReplacement<{ a: DInstruction<undefined> }>>>(true);
assert<IsNever<WholesomeReplacement<{ a: Overloaded }>>>(true);
Loading

0 comments on commit 4c8249a

Please sign in to comment.