From e27183a9e8f6bb5ea12936f133796846af945154 Mon Sep 17 00:00:00 2001 From: Zachary Haber Date: Wed, 16 Aug 2023 13:48:14 -0500 Subject: [PATCH] Make `assert`, `truthy` and `falsy` typeguards Co-authored-by: Sindre Sorhus --- .../assertions-as-type-guards.cts | 28 +++++++++++++++++++ .../module/assertions-as-type-guards.ts | 28 +++++++++++++++++++ types/assertions.d.cts | 17 +++++++++-- 3 files changed, 70 insertions(+), 3 deletions(-) diff --git a/test-types/import-in-cts/assertions-as-type-guards.cts b/test-types/import-in-cts/assertions-as-type-guards.cts index 3ada9756a..ceed64a4a 100644 --- a/test-types/import-in-cts/assertions-as-type-guards.cts +++ b/test-types/import-in-cts/assertions-as-type-guards.cts @@ -4,6 +4,15 @@ import {expectType} from 'tsd'; type Expected = {foo: 'bar'}; const expected: Expected = {foo: 'bar'}; +test('assert', t => { + const actual = expected as Expected | undefined; + if (t.truthy(actual)) { + expectType(actual); + } else { + expectType(actual); + } +}); + test('deepEqual', t => { const actual: unknown = {}; if (t.deepEqual(actual, expected)) { @@ -32,9 +41,28 @@ test('false', t => { } }); +test('falsy', t => { + type Actual = Expected | undefined | false | 0 | '' | 0n; + const actual = undefined as Actual; + if (t.falsy(actual)) { + expectType>(actual); + } else { + expectType(actual); + } +}); + test('true', t => { const actual: unknown = false; if (t.true(actual)) { expectType(actual); } }); + +test('truthy', t => { + const actual = expected as Expected | undefined; + if (t.truthy(actual)) { + expectType(actual); + } else { + expectType(actual); + } +}); diff --git a/test-types/module/assertions-as-type-guards.ts b/test-types/module/assertions-as-type-guards.ts index 36763b98f..9d9c60ad9 100644 --- a/test-types/module/assertions-as-type-guards.ts +++ b/test-types/module/assertions-as-type-guards.ts @@ -5,6 +5,15 @@ import test from '../../entrypoints/main.mjs'; type Expected = {foo: 'bar'}; const expected: Expected = {foo: 'bar'}; +test('assert', t => { + const actual = expected as Expected | undefined; + if (t.truthy(actual)) { + expectType(actual); + } else { + expectType(actual); + } +}); + test('deepEqual', t => { const actual: unknown = {}; if (t.deepEqual(actual, expected)) { @@ -33,9 +42,28 @@ test('false', t => { } }); +test('falsy', t => { + type Actual = Expected | undefined | false | 0 | '' | 0n; + const actual = undefined as Actual; + if (t.falsy(actual)) { + expectType>(actual); + } else { + expectType(actual); + } +}); + test('true', t => { const actual: unknown = false; if (t.true(actual)) { expectType(actual); } }); + +test('truthy', t => { + const actual = expected as Expected | undefined; + if (t.truthy(actual)) { + expectType(actual); + } else { + expectType(actual); + } +}); diff --git a/types/assertions.d.cts b/types/assertions.d.cts index 009cd139e..59a284af0 100644 --- a/types/assertions.d.cts +++ b/types/assertions.d.cts @@ -27,6 +27,8 @@ export type Assertions = { /** * Assert that `actual` is [truthy](https://developer.mozilla.org/en-US/docs/Glossary/Truthy), returning a boolean * indicating whether the assertion passed. + * + * Note: An `else` clause using this as a type guard will be subtly incorrect for `string` and `number` types and will not give `0` or `''` as a potential value in an `else` clause. */ assert: AssertAssertion; @@ -121,16 +123,23 @@ export type Assertions = { /** * Assert that `actual` is [truthy](https://developer.mozilla.org/en-US/docs/Glossary/Truthy), returning a boolean * indicating whether the assertion passed. + * + * Note: An `else` clause using this as a type guard will be subtly incorrect for `string` and `number` types and will not give `0` or `''` as a potential value in an `else` clause. */ truthy: TruthyAssertion; }; +type FalsyValue = false | 0 | 0n | '' | null | undefined; +type Falsy = T extends Exclude ? (T extends number | string | bigint ? T & FalsyValue : never) : T; + export type AssertAssertion = { /** * Assert that `actual` is [truthy](https://developer.mozilla.org/en-US/docs/Glossary/Truthy), returning a boolean * indicating whether the assertion passed. + * + * Note: An `else` clause using this as a type guard will be subtly incorrect for `string` and `number` types and will not give `0` or `''` as a potential value in an `else` clause. */ - (actual: any, message?: string): boolean; + (actual: T, message?: string): actual is T extends Falsy ? never : T; /** Skip this assertion. */ skip(actual: any, message?: string): void; @@ -192,7 +201,7 @@ export type FalsyAssertion = { * Assert that `actual` is [falsy](https://developer.mozilla.org/en-US/docs/Glossary/Falsy), returning a boolean * indicating whether the assertion passed. */ - (actual: any, message?: string): boolean; + (actual: T, message?: string): actual is Falsy; /** Skip this assertion. */ skip(actual: any, message?: string): void; @@ -336,8 +345,10 @@ export type TruthyAssertion = { /** * Assert that `actual` is [truthy](https://developer.mozilla.org/en-US/docs/Glossary/Truthy), returning a boolean * indicating whether the assertion passed. + * + * Note: An `else` clause using this as a type guard will be subtly incorrect for `string` and `number` types and will not give `0` or `''` as a potential value in an `else` clause. */ - (actual: any, message?: string): boolean; + (actual: T, message?: string): actual is T extends Falsy ? never : T; /** Skip this assertion. */ skip(actual: any, message?: string): void;