Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Migrate type tests to TSTyche #2725

Draft
wants to merge 11 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
2 changes: 2 additions & 0 deletions .changeset/weak-apricots-kneel.md
@@ -0,0 +1,2 @@
---
---
13 changes: 11 additions & 2 deletions .eslintrc.js
Expand Up @@ -73,8 +73,8 @@ module.exports = {
files: [
'**/tests/**/*.ts',
'**/tests/**/*.tsx',
'**/*.test-d.ts',
'**/*.test-d.tsx',
'**/*.tst.ts',
'**/*.tst.tsx',
],
rules: {
// We disable `import/no-extraneous-dependencies` for test files because it
Expand All @@ -99,5 +99,14 @@ module.exports = {
'import/order': 'off',
},
},
{
files: [
'**/*.tst.ts',
'**/*.tst.tsx',
],
rules: {
'jest/valid-expect': 'off',
},
},
],
};
3 changes: 3 additions & 0 deletions .github/workflows/node-ci.yml
Expand Up @@ -58,6 +58,9 @@ jobs:
yarn lint
yarn ci:lint-docs

- name: Type tests
run: yarn test:types

- name: Unit tests
run: yarn test "^(?:(?!(address|react-server|web-worker)).)*$"
env:
Expand Down
4 changes: 2 additions & 2 deletions config/typescript/tsconfig.base.json
Expand Up @@ -14,13 +14,13 @@

// Strictness
// Strict mode enables these options.
// Disable them temporarily while we fix up problematic behaviour from
// Disable them temporarily while we fix up problematic behavior from
// the time before we wanted strict mode
"strictBindCallApply": false,
"strictFunctionTypes": false,
"noImplicitAny": false,
"useUnknownInCatchVariables": false,
// These values are are not controled by strict mode, though they are
// These values are not controlled by strict mode, though they are
// enabled in the 'strictest' config that we inherit from.
// These reach a level of pedanticness we aren't worried about
"noImplicitOverride": false,
Expand Down
113 changes: 72 additions & 41 deletions documentation/testing-exported-typescript-types.md
@@ -1,10 +1,12 @@
# Testing exported TypeScript types

## Setting up tsd tests
We use [`tstyche`](https://github.com/tstyche/tstyche) to test TypeScript types. Visit [https://tstyche.org](https://tstyche.org) to view the full documentation of the tool.

We use [jest-runner-tsd](https://github.com/jest-community/jest-runner-tsd) to test TypeScript types. Type test filenames end in `.test-d.ts` (similar to how `.test.ts` is used to denote runtime tests). Create a `test-d` folder in a package and populate it with your `.test-d.ts` test files.
## Setting up type tests

Example:
Type test filenames have the `.tst.*` suffix, similar to how `.test.*` is used to denote runtime tests. Differently from the functional tests, the type test files are only statically analyzed by the TypeScript compiler, but not executed (hence the missing `e` in the suffix).

To create a type test project in a package, add a `typetests` folder with `tsconfig.json` file and populate it with your `.tst.ts` test files:

```
📂 packages
Expand All @@ -15,68 +17,97 @@ Example:
│ └── types.ts
├──── 📂 tests (runtime tests)
│ └── create.test.ts
└──── 📂 test-d (typescript definition tests)
└── types.test-d.ts
└──── 📂 typetests (type tests)
│ ├── create.tst.ts
│ └── tsconfig.json
```

Type tests are ran as part of the standard `yarn test` jest execution.
The TSConfig file will be used by a code editor and TSTyche. It can extend the `tsconfig.base.json` file:

```json
{
"extends": "../../../config/typescript/tsconfig.base.json",
"compilerOptions": {
"emitDeclarationOnly": false,
"strict": true,
"types": []
},
"references": [{"path": ".."}]
}
```

To run the type tests, use the `yarn test:types` command.

## Useful tests

Assuming you have some source types in `./src/types.ts`
TSTyche compares types using the familiar `expect` style assertions (to learn more, see: [https://tstyche.org/reference/expect-api](https://tstyche.org/reference/expect-api)).

```tsx
export interface Person {
firstName: string;
}
For example, here is how the `ArrayElement` utility type is tested:

export type ArrayElement<T> = T extends (infer U)[] ? U : never;
```
```ts
import {describe, it, expect} from 'tstyche';

and an exported function in `./src/create.ts`
import type {ArrayElement} from '../src/types';

```tsx
import {Person} from './types';
describe('ArrayElement', () => {
it('infers the array element type', () => {
expect<ArrayElement<(string | boolean)[]>>().type.toEqual<
string | boolean
>();

export function createPerson(input?: Person): Person {
return {
firstName: input?.firstName ?? 'bob',
};
}
expect<ArrayElement<string[]>>().type.toBeString();
expect<ArrayElement<any[]>>().type.toBeAny();
});

it('when `T` is not an array, resolves to the `never` type', () => {
expect<ArrayElement<string>>().type.toBeNever();
});
});
```

You can test types using [`tsd-lite`](https://github.com/mrazauskas/tsd-lite)'s assertion methods.
If the resulting type is more complex, the `.toMatch()` matcher can be helpful to test partial match:

Check positive scenarios, that a value of a given type matches or is assignable to your value. Use the most strict assertion possible.
```ts
import {describe, it, expect} from 'tstyche';

```tsx
import {expectType, expectAssignable} from 'tsd-lite';
import type {DeepReadonly} from '../src/types';

import type {ArrayElement, Person} from '../src/types';
import {createPerson} from '../src/create';
describe('DeepReadonly', () => {
interface Person {
firstName: string;
lastName?: string | undefined;
}

// strict checks value type equality
expectType<ArrayElement<Person[]>>(createPerson());
it('marks the properties as readonly recursively', () => {
expect<DeepReadonly<Person>>().type.toMatch<{
readonly firstName: string;
}>();

// loose checks value assignable to type
expectAssignable<ArrayElement<string[]>>('foo');
expect<DeepReadonly<Person>>().type.toMatch<{
readonly lastName?: string | undefined;
}>();
});
});
```

Also check that values which do not match your type are not assignable, especially when you include conditional logic in a generic type.
If a generic type has conditional logic, remember to cover all the branches. You can negate the condition by prepend `.not` before a matcher:

```tsx
import {expectError, expectNotAssignable} from 'tsd-lite';
```ts
import {describe, it, expect} from 'tstyche';

import type {ArrayElement} from '../src/types';
import {createPerson} from '../src/create';
import type {IfEmptyObject} from '../src/types';

// string is not a member of an array, so we should not be able to assign anything to it.
expectNotAssignable<ArrayElement<string>>('string');
describe('IfEmptyObject', () => {
it('checks if an object is empty', () => {
expect<IfEmptyObject<{}, true>>().type.toEqual<true>();
expect<IfEmptyObject<{foo: string}, never, false>>().type.toEqual<false>();

// createPerson expects an input of Person where firstName is a string. We pass a boolean, hence the expression will have a type error.
expectError(createPerson({firstName: true}));
expect<IfEmptyObject<{foo: string}, true>>().type.not.toEqual<true>();
expect<IfEmptyObject<boolean, true>>().type.not.toEqual<true>();
});
});
```

## Why not use `yarn type-check`?

Typechecking quilt source code ensures that there are no type errors in the source code. TSD allows us to assert that types built by quilt packages implement the logical constraints we intend them to. You can test error cases, negative cases, and a range of positive cases. This leads to more resilient types and easier refactoring of types.
Typechecking quilt source code ensures that there are no type errors in the source code. TSTyche allows us to assert that types built by quilt packages implement the logical constraints we intend them to. You can test error cases, negative cases, and a range of positive cases. This leads to more resilient types and easier refactoring.
10 changes: 0 additions & 10 deletions jest.config.js
Expand Up @@ -93,15 +93,6 @@ function project(packageName, overrideOptions = {}) {
};
}

function typesProject(packageName, overrideOptions = {}) {
return project(packageName, {
displayName: {name: packageName, color: 'blue'},
runner: 'jest-runner-tsd',
testRegex: ['.+\\.test-d\\.(ts|tsx)$'],
...overrideOptions,
});
}

module.exports = {
cacheDirectory: `${root}/.cache/jest`,
watchPlugins: [
Expand All @@ -115,7 +106,6 @@ module.exports = {
// Everything else is the `packages/*` folders
...packageMapping.flatMap(({name}) => [
project(name, configOverrides[name]),
typesProject(name, configOverrides[name]),
]),
],
};
4 changes: 2 additions & 2 deletions package.json
Expand Up @@ -11,6 +11,7 @@
"type-check": "tsc --build",
"ci:lint-docs": "yarn generate docs && ./scripts/check-docs.sh",
"test": "jest",
"test:types": "tstyche",
"release": "tsc --build && node ./scripts/build.mjs && changeset publish",
"clean": "rimraf './packages/*/build' './packages/*/!(rollup.config).{d.ts,js,esnext,mjs}' '.cache'",
"generate": "plop",
Expand Down Expand Up @@ -48,7 +49,6 @@
"@shopify/eslint-plugin": "^42.0.1",
"@tsconfig/node-lts": "^18.12.1",
"@tsconfig/strictest": "^2.0.0",
"@tsd/typescript": "^4.9.5",
"@types/babel__core": "^7.1.7",
"@types/jest": "^29.4.0",
"@types/react": "^18.0.0",
Expand All @@ -66,7 +66,6 @@
"jest": "^29.4.2",
"jest-environment-jsdom": "^29.4.2",
"jest-extended": "^3.2.3",
"jest-runner-tsd": "^4.0.0",
"jest-watch-typeahead": "^2.2.2",
"npm-run-all": "^4.1.5",
"plop": "^2.6.0",
Expand All @@ -81,6 +80,7 @@
"rimraf": "^2.6.2",
"rollup": "^2.60.1",
"rollup-plugin-node-externals": "^2.2.0",
"tstyche": "^1.1.0",
"typescript": "~5.0.2",
"yalc": "^1.0.0-pre.50"
},
Expand Down
8 changes: 0 additions & 8 deletions packages/admin-graphql-api-utilities/test-d/tsconfig.json

This file was deleted.

29 changes: 0 additions & 29 deletions packages/admin-graphql-api-utilities/test-d/types.test-d.ts

This file was deleted.

@@ -0,0 +1,38 @@
import {describe, it, expect} from 'tstyche';

import type {Gid, ShopifyGid} from '../src';
import {composeGid, composeGidFactory} from '../src';

describe('composeGid', () => {
it('composes Gid using key and number id', () => {
expect(composeGid('Customers', 123)).type.toEqual<
ShopifyGid<'Customers'>
>();
expect(composeGid('Customers', 123)).type.toEqual<
Gid<'shopify', 'Customers'>
>();

expect(composeGid('Customers', 123)).type.toMatch<string>();
expect(composeGid('Customers', 123)).type.not.toMatch<
ShopifyGid<'Orders'>
>();
});
});

describe('composeGidFactory', () => {
it('composes custom Gid using key and number id', () => {
const composeCustomGid = composeGidFactory('custom-app');

expect(composeCustomGid('Customers', 123)).type.not.toEqual<
ShopifyGid<'Customers'>
>();
expect(composeCustomGid('Customers', 123)).type.toEqual<
Gid<'custom-app', 'Customers'>
>();

expect(composeCustomGid('Customers', 123)).type.toMatch<string>();
expect(composeGid('Customers', 123)).type.not.toMatch<
Gid<'custom-app', 'Orders'>
>();
});
});
17 changes: 17 additions & 0 deletions packages/admin-graphql-api-utilities/typetests/tsconfig.json
@@ -0,0 +1,17 @@
{
"extends": "../../../config/typescript/tsconfig.base.json",
"compilerOptions": {
"emitDeclarationOnly": false,
"strict": true,
"strictBindCallApply": true,
"strictFunctionTypes": true,
"strictNullChecks": true,
"exactOptionalPropertyTypes": true,

"types": []
},
"include": ["**/*"],
"exclude": [],

"references": [{"path": ".."}]
}
6 changes: 0 additions & 6 deletions packages/useful-types/test-d/tsconfig.json

This file was deleted.