Skip to content

Commit e27183a

Browse files
Make assert, truthy and falsy typeguards
Co-authored-by: Sindre Sorhus <[email protected]>
1 parent e58f466 commit e27183a

File tree

3 files changed

+70
-3
lines changed

3 files changed

+70
-3
lines changed

test-types/import-in-cts/assertions-as-type-guards.cts

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,15 @@ import {expectType} from 'tsd';
44
type Expected = {foo: 'bar'};
55
const expected: Expected = {foo: 'bar'};
66

7+
test('assert', t => {
8+
const actual = expected as Expected | undefined;
9+
if (t.truthy(actual)) {
10+
expectType<Expected>(actual);
11+
} else {
12+
expectType<undefined>(actual);
13+
}
14+
});
15+
716
test('deepEqual', t => {
817
const actual: unknown = {};
918
if (t.deepEqual(actual, expected)) {
@@ -32,9 +41,28 @@ test('false', t => {
3241
}
3342
});
3443

44+
test('falsy', t => {
45+
type Actual = Expected | undefined | false | 0 | '' | 0n;
46+
const actual = undefined as Actual;
47+
if (t.falsy(actual)) {
48+
expectType<Exclude<Actual, Expected>>(actual);
49+
} else {
50+
expectType<Expected>(actual);
51+
}
52+
});
53+
3554
test('true', t => {
3655
const actual: unknown = false;
3756
if (t.true(actual)) {
3857
expectType<true>(actual);
3958
}
4059
});
60+
61+
test('truthy', t => {
62+
const actual = expected as Expected | undefined;
63+
if (t.truthy(actual)) {
64+
expectType<Expected>(actual);
65+
} else {
66+
expectType<undefined>(actual);
67+
}
68+
});

test-types/module/assertions-as-type-guards.ts

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,15 @@ import test from '../../entrypoints/main.mjs';
55
type Expected = {foo: 'bar'};
66
const expected: Expected = {foo: 'bar'};
77

8+
test('assert', t => {
9+
const actual = expected as Expected | undefined;
10+
if (t.truthy(actual)) {
11+
expectType<Expected>(actual);
12+
} else {
13+
expectType<undefined>(actual);
14+
}
15+
});
16+
817
test('deepEqual', t => {
918
const actual: unknown = {};
1019
if (t.deepEqual(actual, expected)) {
@@ -33,9 +42,28 @@ test('false', t => {
3342
}
3443
});
3544

45+
test('falsy', t => {
46+
type Actual = Expected | undefined | false | 0 | '' | 0n;
47+
const actual = undefined as Actual;
48+
if (t.falsy(actual)) {
49+
expectType<Exclude<Actual, Expected>>(actual);
50+
} else {
51+
expectType<Expected>(actual);
52+
}
53+
});
54+
3655
test('true', t => {
3756
const actual: unknown = false;
3857
if (t.true(actual)) {
3958
expectType<true>(actual);
4059
}
4160
});
61+
62+
test('truthy', t => {
63+
const actual = expected as Expected | undefined;
64+
if (t.truthy(actual)) {
65+
expectType<Expected>(actual);
66+
} else {
67+
expectType<undefined>(actual);
68+
}
69+
});

types/assertions.d.cts

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,8 @@ export type Assertions = {
2727
/**
2828
* Assert that `actual` is [truthy](https://developer.mozilla.org/en-US/docs/Glossary/Truthy), returning a boolean
2929
* indicating whether the assertion passed.
30+
*
31+
* 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.
3032
*/
3133
assert: AssertAssertion;
3234

@@ -121,16 +123,23 @@ export type Assertions = {
121123
/**
122124
* Assert that `actual` is [truthy](https://developer.mozilla.org/en-US/docs/Glossary/Truthy), returning a boolean
123125
* indicating whether the assertion passed.
126+
*
127+
* 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.
124128
*/
125129
truthy: TruthyAssertion;
126130
};
127131

132+
type FalsyValue = false | 0 | 0n | '' | null | undefined;
133+
type Falsy<T> = T extends Exclude<T, FalsyValue> ? (T extends number | string | bigint ? T & FalsyValue : never) : T;
134+
128135
export type AssertAssertion = {
129136
/**
130137
* Assert that `actual` is [truthy](https://developer.mozilla.org/en-US/docs/Glossary/Truthy), returning a boolean
131138
* indicating whether the assertion passed.
139+
*
140+
* 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.
132141
*/
133-
(actual: any, message?: string): boolean;
142+
<T>(actual: T, message?: string): actual is T extends Falsy<T> ? never : T;
134143

135144
/** Skip this assertion. */
136145
skip(actual: any, message?: string): void;
@@ -192,7 +201,7 @@ export type FalsyAssertion = {
192201
* Assert that `actual` is [falsy](https://developer.mozilla.org/en-US/docs/Glossary/Falsy), returning a boolean
193202
* indicating whether the assertion passed.
194203
*/
195-
(actual: any, message?: string): boolean;
204+
<T>(actual: T, message?: string): actual is Falsy<T>;
196205

197206
/** Skip this assertion. */
198207
skip(actual: any, message?: string): void;
@@ -336,8 +345,10 @@ export type TruthyAssertion = {
336345
/**
337346
* Assert that `actual` is [truthy](https://developer.mozilla.org/en-US/docs/Glossary/Truthy), returning a boolean
338347
* indicating whether the assertion passed.
348+
*
349+
* 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.
339350
*/
340-
(actual: any, message?: string): boolean;
351+
<T>(actual: T, message?: string): actual is T extends Falsy<T> ? never : T;
341352

342353
/** Skip this assertion. */
343354
skip(actual: any, message?: string): void;

0 commit comments

Comments
 (0)