diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index e8d3804..a550773 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -3,16 +3,18 @@ on: [push, pull_request] jobs: test: + strategy: + fail-fast: false + matrix: + ts-version: ['4.1', '4.2', '4.3'] + name: TypeScript ${{ matrix.ts-version }} runs-on: ubuntu-latest steps: - - - uses: actions/checkout@v2 - - - name: Use Node.js 12.x + - uses: actions/checkout@v2 + - name: Use Node.js 12.x uses: actions/setup-node@v1 with: node-version: 12.x - - - run: npm install - - - run: npm test + - run: npm install + - run: npm install --save-dev typescript@~${{ matrix.ts-version }} + - run: npm test diff --git a/.xo-config.json b/.xo-config.json index 4129d7a..b29756e 100644 --- a/.xo-config.json +++ b/.xo-config.json @@ -1,16 +1,16 @@ { "rules": { + "unicorn/prevent-abbreviations": "off", + "no-multiple-empty-lines": "off", + "linebreak-style": ["error", "unix"], + "object-curly-spacing": ["error", "always"], + "@typescript-eslint/no-unused-vars": "off", + "@typescript-eslint/no-unsafe-return": "off", "@typescript-eslint/no-empty-function": "off", "@typescript-eslint/ban-types": "off", "@typescript-eslint/indent": "off", - "unicorn/prevent-abbreviations": "off", - "linebreak-style": [ - "error", - "unix" - ], - "object-curly-spacing": [ - "error", - "always" - ] + "@typescript-eslint/object-curly-spacing": ["error", "always"], + "@typescript-eslint/no-unnecessary-type-arguments": "off", + "@typescript-eslint/no-confusing-void-expression": "off" } } diff --git a/package.json b/package.json index ce36461..8b56d65 100644 --- a/package.json +++ b/package.json @@ -35,9 +35,10 @@ }, "devDependencies": { "del-cli": "^3.0.1", + "eslint-config-xo-typescript": "^0.41.1", "np": "^6.5.0", "typescript": "~4.1.3", - "xo": "^0.36.1" + "xo": "^0.39.1" }, "publishConfig": { "access": "public" diff --git a/readme.md b/readme.md index 7ca30da..cf03146 100644 --- a/readme.md +++ b/readme.md @@ -24,14 +24,22 @@ $ npm install @papb/assorted-ts-utils ## Usage ```ts -import { tsAssertTrue } from '@papb/assorted-ts-utils/assert'; import { StrictEqual, ReplaceKeyType, ParseArray, - ReplaceAllTypeOccurrences + ReplaceAllTypeOccurrences, + IfAny, + IfNever } from '@papb/assorted-ts-utils'; +import { + tsAssertTrue, + tsAssertTypeAcceptsValue, + tsAssertTypesExactlyEqual, + tsAssertExtends +} from '@papb/assorted-ts-utils/assert'; + type T1 = StrictEqual<{ a: 1 }, { a: 1 }>; //=> type T1 = true @@ -78,6 +86,15 @@ type T7 = IfAny; type T8 = IfNever<1 & 2, 'hello', 'world'>; //=> type T8 = 'hello'; + +type T9 = { a: string; b: string | number }; +const someValue = + tsAssertTypeAcceptsValue()()( + // Autocomplete for `a` and `b` available on the object below + { a: 'hello', b: 123 } as const + ); +tsAssertTypesExactlyEqual()()<{ a: 'hello'; b: 123 }>(); +tsAssertExtends()()(); ``` diff --git a/source/assert.ts b/source/assert.ts index 64506a7..4a63e10 100644 --- a/source/assert.ts +++ b/source/assert.ts @@ -1,3 +1,36 @@ -export function tsAssertIsSupertype() {} -export function tsAssertTrue() {} -export function tsAssertFalse() {} +import { IfStrictEqual, IfStrictExtends } from '.'; + +const noop = (() => {}); +const identity = ((value: any) => value); + +type Pass = []; +type Fail = [never, never]; + +function tsCreateExactAsserter(): ((...mismatch: IfStrictEqual) => void) { + return noop; +} + +export const tsAssertTypesExactlyEqual = () => tsCreateExactAsserter; +export const tsAssertExtends = () => () => (...mismatch: A extends B ? Pass : Fail) => undefined; +export const tsAssertStrictExtends = () => () => (...mismatch: IfStrictExtends) => undefined; +export const tsAssertIsAssignable = tsAssertExtends; + +export function tsAssertNever(): void; +export function tsAssertNever(never: never): void; +export function tsAssertNever(never?: any): void {} + +export const tsAssertTrue = tsCreateExactAsserter(); +export const tsAssertFalse = tsCreateExactAsserter(); +export const tsAssertAny = tsCreateExactAsserter(); +export const tsAssertUnknown = tsCreateExactAsserter(); + +function tsCreateExactInferAsserter(): ((value: T, ...mismatch: IfStrictEqual) => T) { + return identity as any; +} + +function tsCreateAssignableInferAsserter(): ((value: T) => T) { + return identity as any; +} + +export const tsAssertTypeExactlyDescribesValue = () => tsCreateExactInferAsserter; +export const tsAssertTypeAcceptsValue = () => tsCreateAssignableInferAsserter; diff --git a/source/index.ts b/source/index.ts index 5578200..f19225b 100644 --- a/source/index.ts +++ b/source/index.ts @@ -14,7 +14,8 @@ export type IfNever = [T] extends [never] ? A : B; export type IfAny = 0 extends (1 & T) ? A : B; export type IfUnknown = [unknown] extends [T] ? IfAny : B; -export type StrictExtends = [A] extends [B] ? true : false; +export type IfStrictExtends = [A] extends [B] ? ThenType : ElseType; +export type StrictExtends = IfStrictExtends; export type BidirectionalStrictExtends = And<[StrictExtends, StrictExtends]>; export type IfNotExtendsCoalesce = @@ -70,12 +71,6 @@ export type StrictEqual = IfStrictEqual; /// ------------------------------------------------------------------------------------- -export type IfIdenticalInternalTSRepresentation = - (() => [T] extends [A] ? 1 : 2) extends - (() => [T] extends [B] ? 1 : 2) ? ThenType : ElseType; - -/// ------------------------------------------------------------------------------------- - export type ReplaceAllTypeOccurrences = IfStrictEqual(); +tsAssertUnknown(); + + +// @ts-expect-error +tsAssertAny(); +// @ts-expect-error +tsAssertAny(); +// @ts-expect-error +tsAssertAny(); +// @ts-expect-error +tsAssertAny(); +// @ts-expect-error +tsAssertAny(); +// @ts-expect-error +tsAssertAny<{}>(); +// @ts-expect-error +tsAssertAny(); + + +// @ts-expect-error +tsAssertAny(0 as any); // eslint-disable-line @typescript-eslint/no-unsafe-argument + + +// @ts-expect-error +tsAssertUnknown(); +// @ts-expect-error +tsAssertUnknown(); +// @ts-expect-error +tsAssertUnknown(); +// @ts-expect-error +tsAssertUnknown(); +// @ts-expect-error +tsAssertUnknown(); +// @ts-expect-error +tsAssertUnknown<{}>(); +// @ts-expect-error +tsAssertUnknown(); + + +// @ts-expect-error +tsAssertUnknown(0 as unknown); diff --git a/test/assert/never.test.ts b/test/assert/never.test.ts new file mode 100644 index 0000000..7811eaf --- /dev/null +++ b/test/assert/never.test.ts @@ -0,0 +1,29 @@ +import { tsAssertNever } from '../../source/assert'; + + +declare const neverValue: never; +tsAssertNever(); +tsAssertNever(neverValue); +tsAssertNever(0 as unknown as never); + + +// @ts-expect-error +tsAssertNever(); +// @ts-expect-error +tsAssertNever(); +// @ts-expect-error +tsAssertNever(); +// @ts-expect-error +tsAssertNever<{}>(); +// @ts-expect-error +tsAssertNever(); +// @ts-expect-error +tsAssertNever(); + + +// @ts-expect-error +tsAssertNever(undefined); +// @ts-expect-error +tsAssertNever(null); +// @ts-expect-error +tsAssertNever({}); diff --git a/test/assert/true-false.test.ts b/test/assert/true-false.test.ts new file mode 100644 index 0000000..8903c02 --- /dev/null +++ b/test/assert/true-false.test.ts @@ -0,0 +1,53 @@ +import { tsAssertTrue, tsAssertFalse } from '../../source/assert'; + + +tsAssertTrue(); +tsAssertFalse(); + + +// @ts-expect-error +tsAssertTrue(); +// @ts-expect-error +tsAssertTrue(); +// @ts-expect-error +tsAssertTrue(); +// @ts-expect-error +tsAssertTrue(); +// @ts-expect-error +tsAssertTrue(); +// @ts-expect-error +tsAssertTrue<{}>(); +// @ts-expect-error +tsAssertTrue(); +// @ts-expect-error +tsAssertTrue(); + + +// @ts-expect-error +tsAssertTrue(true); +// @ts-expect-error +tsAssertTrue(true as const); + + +// @ts-expect-error +tsAssertFalse(); +// @ts-expect-error +tsAssertFalse(); +// @ts-expect-error +tsAssertFalse(); +// @ts-expect-error +tsAssertFalse(); +// @ts-expect-error +tsAssertFalse(); +// @ts-expect-error +tsAssertFalse<{}>(); +// @ts-expect-error +tsAssertFalse(); +// @ts-expect-error +tsAssertFalse(); + + +// @ts-expect-error +tsAssertFalse(false); +// @ts-expect-error +tsAssertFalse(false as const); diff --git a/test/assert/type-comparisons.test.ts b/test/assert/type-comparisons.test.ts new file mode 100644 index 0000000..fd0bb09 --- /dev/null +++ b/test/assert/type-comparisons.test.ts @@ -0,0 +1,33 @@ +import { tsAssertTypeAcceptsValue, tsAssertTypesExactlyEqual, tsAssertExtends, tsAssertStrictExtends } from '../../source/assert'; + +type T1 = { a: string; b: string | number }; + +const x = tsAssertTypeAcceptsValue()()( + { a: 'adfasdf', b: 123 } as const +); + +tsAssertTypesExactlyEqual()()<{ a: 'adfasdf'; b: 123 }>(); +tsAssertExtends()()(); +tsAssertStrictExtends()()(); + +// @ts-expect-error +tsAssertTypesExactlyEqual()()(); +// @ts-expect-error +tsAssertExtends()()(); +// @ts-expect-error +tsAssertStrictExtends()()(); + +const y = tsAssertTypeAcceptsValue()()( + { a: 'adfasdf', b: 123 } +); + +tsAssertTypesExactlyEqual()()<{ a: string; b: number }>(); +tsAssertExtends()()(); +tsAssertStrictExtends()()(); + +// @ts-expect-error +tsAssertTypesExactlyEqual()()(); +// @ts-expect-error +tsAssertExtends()()(); +// @ts-expect-error +tsAssertStrictExtends()()(); diff --git a/test/strict-equal.test.ts b/test/strict-equal.test.ts index 97aedb1..ab78fab 100644 --- a/test/strict-equal.test.ts +++ b/test/strict-equal.test.ts @@ -6,7 +6,6 @@ tsAssertTrue>(); tsAssertTrue>(); tsAssertTrue number, (this: unknown) => number>>(); tsAssertTrue>(); -(() => tsAssertTrue>())(); tsAssertFalse>(); tsAssertFalse>();