From f106e376e10e3939293ac0f96fa3a4608f1ad491 Mon Sep 17 00:00:00 2001 From: Zachary Soare Date: Mon, 14 Aug 2023 12:28:40 -0500 Subject: [PATCH 1/6] Make `assert`, `truthy` and `falsy` typeguards --- .../assertions-as-type-guards.cts | 28 +++++++++++++++++++ .../module/assertions-as-type-guards.ts | 28 +++++++++++++++++++ types/assertions.d.cts | 8 ++++-- 3 files changed, 61 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..8897d6ed3 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 | null | 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..5602d8c6d 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 | null | 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..25d37ab16 100644 --- a/types/assertions.d.cts +++ b/types/assertions.d.cts @@ -125,12 +125,14 @@ export type Assertions = { truthy: TruthyAssertion; }; +type Falsy = false | 0 | -0 | 0n | '' | null | undefined; + 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. */ - (actual: any, message?: string): boolean; + (actual: T, message?: string): actual is Exclude; /** Skip this assertion. */ skip(actual: any, message?: string): void; @@ -192,7 +194,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 T extends Falsy ? T :never; /** Skip this assertion. */ skip(actual: any, message?: string): void; @@ -337,7 +339,7 @@ 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. */ - (actual: any, message?: string): boolean; + (actual: T, message?: string): actual is Exclude; /** Skip this assertion. */ skip(actual: any, message?: string): void; From 8c6696fef7bb22a8deb83763a9be2d0f0d81547b Mon Sep 17 00:00:00 2001 From: Zachary Soare Date: Mon, 14 Aug 2023 13:02:59 -0500 Subject: [PATCH 2/6] remove nulls from tests to fix lint error --- test-types/import-in-cts/assertions-as-type-guards.cts | 2 +- test-types/module/assertions-as-type-guards.ts | 2 +- 2 files changed, 2 insertions(+), 2 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 8897d6ed3..ceed64a4a 100644 --- a/test-types/import-in-cts/assertions-as-type-guards.cts +++ b/test-types/import-in-cts/assertions-as-type-guards.cts @@ -42,7 +42,7 @@ test('false', t => { }); test('falsy', t => { - type Actual = Expected | undefined | null | false | 0 | '' | 0n; + type Actual = Expected | undefined | false | 0 | '' | 0n; const actual = undefined as Actual; if (t.falsy(actual)) { expectType>(actual); diff --git a/test-types/module/assertions-as-type-guards.ts b/test-types/module/assertions-as-type-guards.ts index 5602d8c6d..9d9c60ad9 100644 --- a/test-types/module/assertions-as-type-guards.ts +++ b/test-types/module/assertions-as-type-guards.ts @@ -43,7 +43,7 @@ test('false', t => { }); test('falsy', t => { - type Actual = Expected | undefined | null | false | 0 | '' | 0n; + type Actual = Expected | undefined | false | 0 | '' | 0n; const actual = undefined as Actual; if (t.falsy(actual)) { expectType>(actual); From 35083a903f63928376470b5d68c0f38856a26d08 Mon Sep 17 00:00:00 2001 From: Zachary Soare Date: Mon, 14 Aug 2023 15:46:54 -0500 Subject: [PATCH 3/6] remove -0 as it just resolves to 0 --- types/assertions.d.cts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/types/assertions.d.cts b/types/assertions.d.cts index 25d37ab16..a336c5e5c 100644 --- a/types/assertions.d.cts +++ b/types/assertions.d.cts @@ -125,7 +125,7 @@ export type Assertions = { truthy: TruthyAssertion; }; -type Falsy = false | 0 | -0 | 0n | '' | null | undefined; +type Falsy = false | 0 | 0n | '' | null | undefined; export type AssertAssertion = { /** From 04b0f491bfa1a04c5161e2ca503d435a256eb2e7 Mon Sep 17 00:00:00 2001 From: Zachary Soare Date: Tue, 15 Aug 2023 20:21:02 -0500 Subject: [PATCH 4/6] Improve types for number and string falsy values Add a note mentioning the issues with the types for truthy --- types/assertions.d.cts | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 deletions(-) diff --git a/types/assertions.d.cts b/types/assertions.d.cts index a336c5e5c..7a26cac65 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: with typescript, an `else` clause using this as a typeguard 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,18 +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: with typescript, an `else` clause using this as a typeguard 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 Falsy = false | 0 | 0n | '' | null | undefined; +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: with typescript, an `else` clause using this as a typeguard will be subtly incorrect for string and number types and will not give `0` or `''` as a potential value in an `else` clause */ - (actual: T, message?: string): actual is Exclude; + (actual: T, message?: string): actual is T extends Falsy ? never : T; /** Skip this assertion. */ skip(actual: any, message?: string): void; @@ -194,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: T, message?: string): actual is T extends Falsy ? T :never; + (actual: T, message?: string): actual is Falsy; /** Skip this assertion. */ skip(actual: any, message?: string): void; @@ -338,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: with typescript, an `else` clause using this as a typeguard will be subtly incorrect for string and number types and will not give `0` or `''` as a potential value in an `else` clause */ - (actual: T, message?: string): actual is Exclude; + (actual: T, message?: string): actual is T extends Falsy ? never : T; /** Skip this assertion. */ skip(actual: any, message?: string): void; From 58274c1518345b1444afb13c4c050c533d2f2805 Mon Sep 17 00:00:00 2001 From: Zachary Haber Date: Wed, 16 Aug 2023 07:39:25 -0500 Subject: [PATCH 5/6] Remove explicit mention of typescript Co-authored-by: Sindre Sorhus --- types/assertions.d.cts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/types/assertions.d.cts b/types/assertions.d.cts index 7a26cac65..20f95e224 100644 --- a/types/assertions.d.cts +++ b/types/assertions.d.cts @@ -28,7 +28,7 @@ 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: with typescript, an `else` clause using this as a typeguard will be subtly incorrect for string and number types and will not give `0` or `''` as a potential value in an `else` clause + * 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; From c9ec4b16596644edcdaaefb70397e59feb054882 Mon Sep 17 00:00:00 2001 From: Zachary Soare Date: Wed, 16 Aug 2023 08:34:10 -0500 Subject: [PATCH 6/6] Fix other notes to remove the explicit typescript callout --- types/assertions.d.cts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/types/assertions.d.cts b/types/assertions.d.cts index 20f95e224..59a284af0 100644 --- a/types/assertions.d.cts +++ b/types/assertions.d.cts @@ -124,7 +124,7 @@ 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: with typescript, an `else` clause using this as a typeguard will be subtly incorrect for string and number types and will not give `0` or `''` as a potential value in an `else` clause + * 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; }; @@ -137,7 +137,7 @@ 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: with typescript, an `else` clause using this as a typeguard will be subtly incorrect for string and number types and will not give `0` or `''` as a potential value in an `else` clause + * 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: T, message?: string): actual is T extends Falsy ? never : T; @@ -346,7 +346,7 @@ 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: with typescript, an `else` clause using this as a typeguard will be subtly incorrect for string and number types and will not give `0` or `''` as a potential value in an `else` clause + * 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: T, message?: string): actual is T extends Falsy ? never : T;