Skip to content

Commit

Permalink
Split content into meaningful/digestible sections
Browse files Browse the repository at this point in the history
  • Loading branch information
chriskrycho committed Sep 26, 2023
1 parent 7019914 commit 6f73722
Show file tree
Hide file tree
Showing 17 changed files with 1,039 additions and 917 deletions.
18 changes: 15 additions & 3 deletions config.toml
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,19 @@ description = "A specification for managing changes to TypeScript types, includi
compile_sass = true

# Whether to build a search index to be used later on by a JavaScript library
build_search_index = false
build_search_index = true

# The default language; used in feeds and search index
# Note: the search index doesn't support Chinese/Japanese/Korean Languages
default_language = "en"

[search]
# Whether to include the title of the page/section in the index
include_title = true
# Whether to include the description of the page/section in the index
include_description = false
# Whether to include the rendered content of the page/section in the index
include_content = true

[markdown]
# Whether to do syntax highlighting
Expand All @@ -23,5 +35,5 @@ highlight_theme = "css"
# { theme = "charcoal", filename = "syntax-theme-dark.css" },
# ]

[extra]
# Put all your custom variables here
[link_checker]
internal_level = "warn"
944 changes: 30 additions & 914 deletions content/_index.md

Large diffs are not rendered by default.

10 changes: 10 additions & 0 deletions content/appendices/_index.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
---
title: Appendices
next: /appendices/a-existing-implementations/
---

These sections are non-normative, but provide useful information for developer wanting to use this spec.

- [Appendix A: Existing Implementations](#)
- [Appendix B: Tooling](#)
- [Appendix C: On Variance in TypeScript](#)
22 changes: 22 additions & 0 deletions content/appendices/a-existing-implementations.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
---
title: "Appendix A: Existing Implementations"
---

The recommendations in this RFC have been fully implemented in many packages, especially but not only in the Ember.js ecosystem.

- In 2022, the entire Ember.js project adopted this spec as governing the TypeScript type aspects of [its SemVer contract][ember-semver].

- Some of the earliest adopters (and therefore longest users) of this spec include:
- [`ember-modifier`][ember-modifier]
- [True Myth][true-myth]
- [ember-async-data][ember-async-data]
- [`ember-concurrency`][ember-concurrency]

[ember-semver]: https://emberjs.com/releases/
[ember-modifier]: https://github.com/ember-modifier/ember-modifier
[ember-async-data]: https://github.com/chriskrycho/ember-async-data
[ember-concurrency]: https://github.com/machty/ember-concurrency

`ember-modifier`, `ember-async-data`, and `true-myth` all publish types generated from implementation code.
`ember-concurrency` supplies a standalone, hand-written type definition file.
Since adopting this policy in these implementations (beginning in early summer 2020), no known issues have emerged, and the experience of implementing earlier versions of the recommendations from this spec have been incorporated into the final form of the spec.
85 changes: 85 additions & 0 deletions content/appendices/b-tooling.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
---
title: "Appendix B: Tooling"
prev: /appendices/a-existing-implementations/
next: /appendices/c-variance-in-typescript/
---

### Appendix B: Tooling

To successfully adopt this RFC’s recommendations, package authors need to be able to *detect* breaking changes (whether from their own changes or from TypeScript itself) and to *mitigate* them. Package *consumers* need to know the support policy for the library.


#### Documenting supported versions and policy

In line with ecosystem norms, badges linking to CI status

- An example supported versions badge (which could link to CI config):

![supported TypeScript versions: 4.1, 4.2 and next](https://img.shields.io/badge/TS%20Versions-4.1%20%7C%204.2%20%7C%20next-blue)

- Example support policy badges (which could link to the published recommendation from this RFC):

![TypeScript support policy](https://img.shields.io/badge/TS%20Support-Rolling%20Window-purple) ![TypeScript support policy](https://img.shields.io/badge/TS%20Support-Simple%20Majors-purple)


#### Detect breaking changes in types

As with runtime code, it is essential to prevent unintentional changes to the API of types supplied by a package. We can accomplish this using *type tests*: tests which assert that the types exposed by the public API of the package are stable.

Package authors publishing types can use whatever tools they find easiest to use and integrate, within the constraint that the tools must be capable of catching all the kinds of breaking changes outlined above. Additionally, they must be able to run against multiple supported versions of TypeScript, so that users can detect and account for breaking changes in TypeScript.

The current options include:

- [`dtslint`][dtslint]—used to support the DefinitelyTyped ecosystem, so it is well-tested and fairly robust. It uses the TypeScript compiler API directly, and is maintained by the TypeScript team directly. Accordingly, it is very unlikely to stop working against the versions of TypeScript it supports. However, there are several significant downsides as well:

- The tool checks against string representations of types, which makes it relatively fragile: it can be disturbed by changes to the representation of a type, even when those changes would not impact type-checking.

- As its name suggests, it is currently powered by [tslint][tslint], which is deprecated in favor of [eslint][eslint]. While there is [initial interest][eslint-migration] in migrating to eslint, there is no active effort to accomplish this task.

- The developer experience of authoring assertions with dtslint is poor, with no editor-powered feedback or indication of whether you've actually written the test correctly at all. For example, if a user types `ExpectType` instead of `$ExpectType`, the assertion will simply be silently ignored.

- [`tsd`][tsd]—a full solution for testing types by writing `.test-d.ts` files with a small family of assertions and using the `tsd` command to validate all `.test-d.ts` files. Authoring has robust editor integration, since the type assertions are normal TS imports, and the type assertions are specific enough to catch all the kinds of breakage identified above. It is implemented using the TS compiler version directly, which makes its assertions fairly robust. Risks and downsides:

- The tool uses a patched version of the TypeScript compiler, which increases the risk of errors and the risk that at some points it will simply be unable to support a new version of TypeScript.

- Because the assertions are implemented as type definitions, the library is subject to the same risk of compiler breakage as the types it is testing.

- **BLOCKER:** currently only supports a single version of TypeScript at a time. While the author is [interested][tsd-versions] in supporting multiple versions, it is not currently possible.

- [`expect-type`][expect-type]—a library with a variety of type assertions, inspired by Jest's matchers but tailored to types and with no runtime implementation. Like `tsd`, it is implemented as a series of function types which can be imported, and accordingly it has excellent editor integration. However, unlike `tsd`, it does *not* use the compiler API. Instead, It is robust enough to catch all the varieties of breaking type changes. The risks with expect-type are:

- It currently has a single maintainer, although it has seen significant uptake since being adopted as part of [Vitest][vitest].

- It is relatively young, having been created only about a year ago, and therefore having existed for only 5 TypeScript releases. While its track record is good so far, there is not yet evidence of how it would deal with serious breaking changes like those introduced in TypeScript 3.5.

- Because the assertions are implemented as type definitions, the library is subject to the same risk of compiler breakage as the types it is testing.

[vitest]: https://vitest.dev

`expect-type` seems to be the best option, and a number of libraries in the TS community are already using `expect-type` successfully (see [**Appendix A**](@./appendices/a-existing-implementations.md) above). However, for the purposes of *this* RFC, we do not make a specific recommendation about which library to use. The tradeoffs above are offered to help authors make an informed choice in this space.

Users should add one of these libraries and generate a set of tests corresponding to their public API. These tests should be written in such a way as to test the imported API as consumers will consume the library. For example, type tests should not import using relative paths, but using the absolute paths at which the types should resolve, just as consumers would.

These type tests should be specific and precise. It is important, for example, to guarantee that an API element never *accidentally* becomes `any`, thereby making many things allowable which should not be in the case of function arguments, and "infecting" the caller's code by eliminating type safety on the result in the case of function return values. For example, the `expect-type` library's `.toEqualTypeOf` assertion is robust against precisely this scenario; package authors are also encouraged to use its `.not` modifier and `.toBeAny()` method where appropriate to prevent this failure mode.

To be safe, these tests should be placed in a directory which does not emit runtime code—either colocated with the library's runtime tests, or in a dedicated `type-tests` directory. Additionally, type tests should *never* export any code.

[dtslint]: https://github.com/microsoft/dtslint
[tslint]: https://github.com/palantir/tslint
[eslint]: https://github.com/eslint/eslint
[eslint-migration]: https://github.com/microsoft/dtslint/issues/300
[tsd]: https://github.com/SamVerschueren/tsd
[tsd-versions]: https://github.com/SamVerschueren/tsd/issues/47
[expect-type]: https://github.com/mmkal/ts/tree/master/packages/expect-type#readme

In addition to *writing* these tests, package authors should make sure to run the tests (as appropriate to the testing tool chosen) in their continuous integration configuration, so that any changes made to the library are validated to make sure the API has not been changed accidentally.

Further, just as packages are encouraged to test against a matrix of peer dependencies versions, they should do likewise with TypeScript. For example:

- Ember packages regularly test against Ember’s current stable release, the currently active Ember LTS release, and the canary and beta releases.
- React libraries regularly test against both the current major, any upcoming major, and sometimes a previous major.
- Node libraries regularly test against all active Node LTS releases and the current stable release.

Along the same lines, TypeScript packages should follow should test the types against all versions of TypeScript supported by the package (see the [suggested policy for version support](#supported-compiler-versions) below) as well as the upcoming version (the `next` tag for the `typescript` package on npm).

These type tests can run as normal CI jobs.
Loading

0 comments on commit 6f73722

Please sign in to comment.