diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 6d61ec989..41349ebbc 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -50,8 +50,8 @@ jobs: - run: npm run type-check - run: npm run check-git-clean - unit-test: - name: 'Build & Unit Test' + test: + name: 'Build & Unit Test & Type Test' runs-on: ubuntu-latest steps: - uses: actions/checkout@v2 @@ -65,7 +65,8 @@ jobs: restore-keys: ${{ runner.OS }}-node- - run: npm ci - run: npm run build - - run: npm run unit-test + - run: npm run test:unit + - run: npm run test:types -- --target 4.5,5.0,current - run: npx size-limit - run: npm run check-git-clean @@ -90,7 +91,7 @@ jobs: publish: name: 'Publish' - needs: [lint, type-check, unit-test, website] + needs: [lint, type-check, test, website] if: github.ref == 'refs/heads/main' runs-on: ubuntu-latest steps: diff --git a/.prettierignore b/.prettierignore index 407b338f4..5ee00f8c0 100644 --- a/.prettierignore +++ b/.prettierignore @@ -1,5 +1,10 @@ dist pages/generated pages/out -type-definitions/ts-tests/ -type-definitions/flow-tests/ \ No newline at end of file +type-definitions/ts-tests/*.ts +type-definitions/flow-tests/ + +!type-definitions/ts-tests/covariance.ts +!type-definitions/ts-tests/deepCopy.ts +!type-definitions/ts-tests/empty.ts +!type-definitions/ts-tests/es6-collections.ts diff --git a/package-lock.json b/package-lock.json index 92c1391e5..a87967ffc 100644 --- a/package-lock.json +++ b/package-lock.json @@ -53,6 +53,7 @@ "rollup": "3.28.1", "size-limit": "^8.2.6", "transducers-js": "0.4.174", + "tstyche": "^1.0.0", "typescript": "5.1" }, "engines": { @@ -13376,6 +13377,29 @@ "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==", "dev": true }, + "node_modules/tstyche": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/tstyche/-/tstyche-1.0.0.tgz", + "integrity": "sha512-iIpktIBuWL0M+KgCO8TIgo+QGS+mD24ll+wAYCFsd8IkA3Lai0ETqGSilHo1PC59+2lf71qJmWShFZwMMH4i1g==", + "dev": true, + "bin": { + "tstyche": "build/bin.js" + }, + "engines": { + "node": "^16.14 || 18.x || >=20.x" + }, + "funding": { + "url": "https://github.com/tstyche/tstyche?sponsor=1" + }, + "peerDependencies": { + "typescript": "4.x || 5.x" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, "node_modules/tsutils": { "version": "2.29.0", "resolved": "https://registry.npmjs.org/tsutils/-/tsutils-2.29.0.tgz", @@ -24148,6 +24172,13 @@ "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==", "dev": true }, + "tstyche": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/tstyche/-/tstyche-1.0.0.tgz", + "integrity": "sha512-iIpktIBuWL0M+KgCO8TIgo+QGS+mD24ll+wAYCFsd8IkA3Lai0ETqGSilHo1PC59+2lf71qJmWShFZwMMH4i1g==", + "dev": true, + "requires": {} + }, "tsutils": { "version": "2.29.0", "resolved": "https://registry.npmjs.org/tsutils/-/tsutils-2.29.0.tgz", diff --git a/package.json b/package.json index eb22adcf9..9f14d03f1 100644 --- a/package.json +++ b/package.json @@ -55,7 +55,9 @@ "npm": ">=7.0.0" }, "scripts": { - "test": "run-s format lint type-check build unit-test", + "test": "run-s format lint type-check build test:*", + "test:unit": "jest", + "test:types": "tstyche", "format": "npm run lint:format -- --write", "lint": "run-s lint:*", "lint:format": "prettier --check \"{__tests__,src,type-definitions,website/src,perf,resources}/**/*{.js,.ts,.tsx,.flow,.css}\"", @@ -69,7 +71,6 @@ "build:types": "cpy ./type-definitions/immutable.* dist", "build:prepare": "./resources/prepare-dist.sh", "build:stats": "node ./resources/dist-stats.mjs", - "unit-test": "jest", "website:build": "cd website && next build && next-sitemap", "website:dev": "cd website && next dev", "check-git-clean": "./resources/check-git-clean.sh", @@ -127,6 +128,7 @@ "rollup": "3.28.1", "size-limit": "^8.2.6", "transducers-js": "0.4.174", + "tstyche": "^1.0.0", "typescript": "5.1" }, "size-limit": [ diff --git a/tstyche.config.json b/tstyche.config.json new file mode 100644 index 000000000..22b2d9784 --- /dev/null +++ b/tstyche.config.json @@ -0,0 +1,9 @@ +{ + "$schema": "https://tstyche.org/schemas/config.json", + "testFileMatch": [ + "**/type-definitions/ts-tests/covariance.ts", + "**/type-definitions/ts-tests/deepCopy.ts", + "**/type-definitions/ts-tests/empty.ts", + "**/type-definitions/ts-tests/es6-collections.ts" + ] +} diff --git a/type-definitions/ts-tests/covariance.ts b/type-definitions/ts-tests/covariance.ts index c1bf50ffc..c23cbb541 100644 --- a/type-definitions/ts-tests/covariance.ts +++ b/type-definitions/ts-tests/covariance.ts @@ -1,4 +1,13 @@ -import { List, Map, OrderedMap, OrderedSet, Set, Stack } from 'immutable'; +import { expect, test } from 'tstyche'; +import { + List, + Map, + MapOf, + OrderedMap, + OrderedSet, + Set, + Stack, +} from 'immutable'; class A { x: number; @@ -7,6 +16,7 @@ class A { this.x = 1; } } + class B extends A { y: string; @@ -15,6 +25,7 @@ class B extends A { this.y = 'B'; } } + class C { z: string; @@ -23,55 +34,52 @@ class C { } } -// List covariance -let listOfB: List = List(); -let listOfA: List = listOfB; - -// $ExpectType List -listOfA = List([new B()]); - -// $ExpectError -let listOfC: List = listOfB; - -// Map covariance -declare let mapOfB: Map; -let mapOfA: Map = mapOfB; - -// $ExpectType MapOf<{ b: B; }> -mapOfA = Map({ b: new B() }); - -// $ExpectError -let mapOfC: Map = mapOfB; - -// Set covariance -declare let setOfB: Set; -let setOfA: Set = setOfB; - -// $ExpectType Set -setOfA = Set([new B()]); -// $ExpectError -let setOfC: Set = setOfB; - -// Stack covariance -declare let stackOfB: Stack; -let stackOfA: Stack = stackOfB; -// $ExpectType Stack -stackOfA = Stack([new B()]); -// $ExpectError -let stackOfC: Stack = stackOfB; - -// OrderedMap covariance -declare let orderedMapOfB: OrderedMap; -let orderedMapOfA: OrderedMap = orderedMapOfB; -// $ExpectType OrderedMap -orderedMapOfA = OrderedMap({ b: new B() }); -// $ExpectError -let orderedMapOfC: OrderedMap = orderedMapOfB; - -// OrderedSet covariance -declare let orderedSetOfB: OrderedSet; -let orderedSetOfA: OrderedSet = orderedSetOfB; -// $ExpectType OrderedSet -orderedSetOfA = OrderedSet([new B()]); -// $ExpectError -let orderedSetOfC: OrderedSet = orderedSetOfB; +test('List covariance', () => { + expect>().type.toBeAssignable(List()); + + expect(List([new B()])).type.toEqual>(); + + expect>().type.not.toBeAssignable(List()); +}); + +test('Map covariance', () => { + expect>().type.toBeAssignable>(); + + expect(Map({ b: new B() })).type.toEqual>(); + + expect>().type.not.toBeAssignable>(); +}); + +test('Set covariance', () => { + expect>().type.toBeAssignable>(); + + expect(Set([new B()])).type.toEqual>(); + + expect>().type.not.toBeAssignable>(); +}); + +test('Stack covariance', () => { + expect>().type.toBeAssignable>(); + + expect(Stack([new B()])).type.toEqual>(); + + expect>().type.not.toBeAssignable>(); +}); + +test('OrderedMap covariance', () => { + expect>().type.toBeAssignable>(); + + expect(OrderedMap({ b: new B() })).type.toEqual>(); + + expect>().type.not.toBeAssignable< + OrderedMap + >(); +}); + +test('OrderedSet covariance', () => { + expect>().type.toBeAssignable>(); + + expect(OrderedSet([new B()])).type.toEqual>(); + + expect>().type.not.toBeAssignable>(); +}); diff --git a/type-definitions/ts-tests/deepCopy.ts b/type-definitions/ts-tests/deepCopy.ts index a3240d3c1..668f00818 100644 --- a/type-definitions/ts-tests/deepCopy.ts +++ b/type-definitions/ts-tests/deepCopy.ts @@ -1,122 +1,112 @@ +import { describe, expect, test } from 'tstyche'; import { List, Map, Record, Set, Seq, DeepCopy, Collection } from 'immutable'; -{ - // Basic types - - // $ExpectType { a: number; b: number; } - type Test = DeepCopy<{ a: number; b: number }>; -} - -{ - // Iterables - - // $ExpectType string[] - type Test = DeepCopy; - - // $ExpectType number[] - type Keyed = DeepCopy>; -} - -{ - // Immutable first-level types - - // $ExpectType { [x: string]: string; } - type StringKey = DeepCopy>; - - // should be `{ [x: string]: object; }` but there is an issue with circular references - // $ExpectType { [x: string]: unknown; } - type ObjectKey = DeepCopy>; - - // should be `{ [x: string]: object; [x: number]: object; }` but there is an issue with circular references - // $ExpectType { [x: string]: unknown; [x: number]: unknown; } - type MixedKey = DeepCopy>; - - // $ExpectType string[] - type ListDeepCopy = DeepCopy>; - - // $ExpectType string[] - type SetDeepCopy = DeepCopy>; -} - -{ - // Keyed - - // $ExpectType { [x: string]: number; } - type Keyed = DeepCopy>; - - // $ExpectType { [x: string]: number; [x: number]: number; } - type KeyedMixed = DeepCopy>; - - // $ExpectType { [x: string]: number; [x: number]: number; } - type KeyedSeqMixed = DeepCopy>; - - // $ExpectType { [x: string]: number; [x: number]: number; } - type MapMixed = DeepCopy>; -} - -{ - // Nested - - // should be `{ map: { [x: string]: string; }; list: string[]; set: string[]; }` but there is an issue with circular references - // $ExpectType { map: unknown; list: unknown; set: unknown; } - type NestedObject = DeepCopy<{ map: Map; list: List; set: Set; }>; - - // should be `{ map: { [x: string]: string; }; }`, but there is an issue with circular references - // $ExpectType { map: unknown; } - type NestedMap = DeepCopy>>; -} - -{ - // Circular references - - type Article = Record<{ title: string; tag: Tag; }>; - type Tag = Record<{ name: string; article: Article; }>; - - // should handle circular references here somehow - // $ExpectType { title: string; tag: unknown; } - type Circular = DeepCopy
; -} - -{ - // Circular references #1957 - - class Foo1 extends Record<{ - foo: undefined | Foo1; - }>({ - foo: undefined - }) { - } - - class Foo2 extends Record<{ - foo?: Foo2; - }>({ - foo: undefined - }) { - } - - class Foo3 extends Record<{ - foo: null | Foo3; - }>({ - foo: null - }) { - } - - // $ExpectType { foo: unknown; } - type DeepFoo1 = DeepCopy; - - // $ExpectType { foo?: unknown; } - type DeepFoo2 = DeepCopy; - - // $ExpectType { foo: unknown; } - type DeepFoo3 = DeepCopy; - - class FooWithList extends Record<{ - foos: undefined | List; - }>({ - foos: undefined - }) { - } - - // $ExpectType { foos: unknown; } - type DeepFooList = DeepCopy; -} +describe('DeepCopy', () => { + test('basic types', () => { + expect< + DeepCopy<{ + a: number; + b: number; + }> + >().type.toEqual<{ + a: number; + b: number; + }>(); + }); + + test('iterables', () => { + expect>().type.toEqual(); + + expect>>().type.toEqual(); + }); + + test('immutable first-level types', () => { + expect>>().type.toEqual<{ + [x: string]: string; + }>(); + + // should be `{ [x: string]: object }`, but there is an issue with circular references + expect>>().type.toEqual<{ + [x: string]: unknown; + }>(); + + // should be `{ [x: string]: object; [x: number]: object }`, but there is an issue with circular references + expect>>().type.toEqual<{ + [x: string]: unknown; + [x: number]: unknown; + }>(); + + expect>>().type.toEqual(); + + expect>>().type.toEqual(); + }); + + test('keyed', () => { + expect>>().type.toEqual<{ + [x: string]: number; + }>(); + + expect>>().type.toEqual<{ + [x: string]: number; + [x: number]: number; + }>(); + + expect>>().type.toEqual<{ + [x: string]: number; + [x: number]: number; + }>(); + + expect>>().type.toEqual<{ + [x: string]: number; + [x: number]: number; + }>(); + }); + + test('nested', () => { + // should be `{ map: { [x: string]: string }; list: string[]; set: string[] }`, but there is an issue with circular references + expect< + DeepCopy<{ + map: Map; + list: List; + set: Set; + }> + >().type.toEqual<{ map: unknown; list: unknown; set: unknown }>(); + + // should be `{ map: { [x: string]: string } }`, but there is an issue with circular references + expect>>>().type.toEqual<{ + map: unknown; + }>(); + }); + + test('circular references', () => { + type Article = Record<{ title: string; tag: Tag }>; + type Tag = Record<{ name: string; article: Article }>; + + // should handle circular references here somehow + expect>().type.toEqual<{ title: string; tag: unknown }>(); + }); + + test('circular references #1957', () => { + class Foo1 extends Record<{ foo: undefined | Foo1 }>({ + foo: undefined, + }) {} + + class Foo2 extends Record<{ foo?: Foo2 }>({ + foo: undefined, + }) {} + + class Foo3 extends Record<{ foo: null | Foo3 }>({ + foo: null, + }) {} + + expect>().type.toEqual<{ foo: unknown }>(); + expect>().type.toEqual<{ foo?: unknown }>(); + expect>().type.toEqual<{ foo: unknown }>(); + + class FooWithList extends Record<{ foo: undefined | List }>({ + foo: undefined, + }) {} + + expect>().type.toEqual<{ foo: unknown }>(); + }); +}); diff --git a/type-definitions/ts-tests/empty.ts b/type-definitions/ts-tests/empty.ts index 289da3fab..f2065e147 100644 --- a/type-definitions/ts-tests/empty.ts +++ b/type-definitions/ts-tests/empty.ts @@ -1,57 +1,44 @@ +import { expect, test } from 'tstyche'; import { Seq, Collection } from 'immutable'; -{ - // Typed empty seqs +test('typed empty Seq', () => { + expect(Seq()).type.toEqual>(); - // $ExpectType Seq - Seq(); + expect(Seq()).type.toEqual>(); - // $ExpectType Seq - Seq(); + expect(Seq.Indexed()).type.toEqual>(); - // $ExpectType Indexed - Seq.Indexed(); + expect(Seq.Indexed()).type.toEqual>(); - // $ExpectType Indexed - Seq.Indexed(); + expect(Seq.Keyed()).type.toEqual>(); - // $ExpectType Keyed - Seq.Keyed(); + expect(Seq.Keyed()).type.toEqual>(); - // $ExpectType Keyed - Seq.Keyed(); + expect(Seq.Set()).type.toEqual>(); - // $ExpectType Set - Seq.Set(); + expect(Seq.Set()).type.toEqual>(); +}); - // $ExpectType Set - Seq.Set(); -} +test('typed empty Collection', () => { + expect(Collection()).type.toEqual>(); -{ - // Typed empty collection + expect(Collection()).type.toEqual< + Collection + >(); - // $ExpectType Collection - Collection(); + expect(Collection.Indexed()).type.toEqual>(); - // $ExpectType Collection - Collection(); + expect(Collection.Indexed()).type.toEqual< + Collection.Indexed + >(); - // $ExpectType Indexed - Collection.Indexed(); + expect(Collection.Keyed()).type.toEqual>(); - // $ExpectType Indexed - Collection.Indexed(); + expect(Collection.Keyed()).type.toEqual< + Collection.Keyed + >(); - // $ExpectType Keyed - Collection.Keyed(); + expect(Collection.Set()).type.toEqual>(); - // $ExpectType Keyed - Collection.Keyed(); - - // $ExpectType Set - Collection.Set(); - - // $ExpectType Set - Collection.Set(); -} + expect(Collection.Set()).type.toEqual>(); +}); diff --git a/type-definitions/ts-tests/es6-collections.ts b/type-definitions/ts-tests/es6-collections.ts index 738c435c6..9fd6eb46b 100644 --- a/type-definitions/ts-tests/es6-collections.ts +++ b/type-definitions/ts-tests/es6-collections.ts @@ -1,18 +1,23 @@ -import { - Map as ImmutableMap, - Set as ImmutableSet, -} from 'immutable'; +import { expect, test } from 'tstyche'; +import { Map as ImmutableMap, Set as ImmutableSet } from 'immutable'; -// Immutable.js collections -const mapImmutable: ImmutableMap = ImmutableMap(); -const setImmutable: ImmutableSet = ImmutableSet(); +test('immutable.js collections', () => { + const mapImmutable: ImmutableMap = ImmutableMap< + string, + number + >(); + const setImmutable: ImmutableSet = ImmutableSet(); -// $ExpectType Map -mapImmutable.delete('foo'); + expect(mapImmutable.delete('foo')).type.toEqual< + ImmutableMap + >(); + expect(setImmutable.delete('bar')).type.toEqual>(); +}); -// ES6 collections -const mapES6: Map = new Map(); -const setES6: Set = new Set(); +test('ES6 collections', () => { + const mapES6: Map = new Map(); + const setES6: Set = new Set(); -// $ExpectType boolean -mapES6.delete('foo'); + expect(mapES6.delete('foo')).type.toEqual(); + expect(setES6.delete('bar')).type.toEqual(); +}); diff --git a/type-definitions/ts-tests/tslint.json b/type-definitions/ts-tests/tslint.json index 674e90cb8..c8ae42edc 100644 --- a/type-definitions/ts-tests/tslint.json +++ b/type-definitions/ts-tests/tslint.json @@ -1,5 +1,8 @@ { "extends": "dtslint/dtslint.json", + "linterOptions": { + "exclude": ["covariance.ts", "deepCopy.ts", "empty.ts", "es6-collections.ts"] + }, "rules": { "no-var": false, "prefer-const": false,