Add ExtendsStrict type (#1165)

Co-authored-by: Som Shekhar Mukherjee <iamssmkhrj@gmail.com>
This commit is contained in:
benz 2025-06-05 17:50:59 +01:00 committed by GitHub
parent b55e2f0459
commit d71242aee4
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 122 additions and 0 deletions

1
index.d.ts vendored
View File

@ -186,5 +186,6 @@ export type {PackageJson} from './source/package-json.d.ts';
export type {TsConfigJson} from './source/tsconfig-json.d.ts';
// Improved built-in
export type {ExtendsStrict} from './source/extends-strict.d.ts';
export type {ExtractStrict} from './source/extract-strict.d.ts';
export type {ExcludeStrict} from './source/exclude-strict.d.ts';

View File

@ -296,6 +296,7 @@ Click the type names for complete docs.
### Improved built-in
- [`ExtendsStrict`](source/extends-strict.d.ts) - A stricter, non-distributive version of `extends` for checking whether one type is assignable to another.
- [`ExtractStrict`](source/extract-strict.d.ts) - A stricter version of `Extract<T, U>` that ensures every member of `U` can successfully extract something from `T`.
- [`ExcludeStrict`](source/exclude-strict.d.ts) - A stricter version of `Exclude<T, U>` that ensures every member of `U` can successfully exclude something from `T`.

42
source/extends-strict.d.ts vendored Normal file
View File

@ -0,0 +1,42 @@
import type {IsNever} from './is-never.d.ts';
import type {IsAny} from './is-any.d.ts';
/**
A stricter, non-distributive version of `extends` for checking whether one type is assignable to another.
Unlike the built-in `extends` keyword, `ExtendsStrict`:
1. Prevents distribution over union types by wrapping both types in tuples. For example, `ExtendsStrict<string | number, number>` returns `false`, whereas `string | number extends number` would result in `boolean`.
2. Treats `never` as a special case: `never` doesn't extend every other type, it only extends itself (or `any`). For example, `ExtendsStrict<never, number>` returns `false` whereas `never extends number` would result in `true`.
@example
```
import type {ExtendsStrict} from 'type-fest';
type T1 = ExtendsStrict<number | string, string>;
//=> false
type T2 = ExtendsStrict<never, number>;
//=> false
type T3 = ExtendsStrict<never, never>;
//=> true
type T4 = ExtendsStrict<string, number | string>;
//=> true
type T5 = ExtendsStrict<string, string>;
//=> true
```
@category Improved Built-in
*/
export type ExtendsStrict<Left, Right> =
IsAny<Left | Right> extends true
? true
: IsNever<Left> extends true
? IsNever<Right>
: [Left] extends [Right]
? true
: false;

78
test-d/extends-strict.ts Normal file
View File

@ -0,0 +1,78 @@
import {expectType} from 'tsd';
import type {Tagged} from '../source/tagged.d.ts';
import type {ExtendsStrict} from '../source/extends-strict.d.ts';
// Basic
expectType<ExtendsStrict<string, string>>(true);
expectType<ExtendsStrict<number, number>>(true);
expectType<ExtendsStrict<1, number>>(true);
expectType<ExtendsStrict<number, 1>>(false);
expectType<ExtendsStrict<'foo', 'foo' | 'bar'>>(true);
expectType<ExtendsStrict<'foo' | 'bar', 'foo'>>(false);
// Union behavior
expectType<ExtendsStrict<string | number, string>>(false);
expectType<ExtendsStrict<string, string | number>>(true);
expectType<ExtendsStrict<string | string[], string>>(false);
expectType<ExtendsStrict<string, string | string[]>>(true);
// Never handling
expectType<ExtendsStrict<never, never>>(true);
expectType<ExtendsStrict<never, string>>(false);
expectType<ExtendsStrict<string, never>>(false);
// Any and unknown
expectType<ExtendsStrict<any, any>>(true);
expectType<ExtendsStrict<any, never>>(true);
expectType<ExtendsStrict<never, any>>(true);
expectType<ExtendsStrict<any, number>>(true);
expectType<ExtendsStrict<any, unknown>>(true); // `any` is assignable to `unknown`
expectType<ExtendsStrict<unknown, any>>(true); // `unknown` is assignable to `any`
expectType<ExtendsStrict<unknown, unknown>>(true);
expectType<ExtendsStrict<string, unknown>>(true);
expectType<ExtendsStrict<unknown, string>>(false);
// Tuples
expectType<ExtendsStrict<[1, 2], number[]>>(true);
expectType<ExtendsStrict<number[], [1, 2]>>(false);
expectType<ExtendsStrict<[], []>>(true);
// Objects
expectType<ExtendsStrict<{a: 1}, {a: number}>>(true);
expectType<ExtendsStrict<{a: number}, {a: 1}>>(false);
expectType<ExtendsStrict<{a: number}, {a: number; b: string}>>(false);
expectType<ExtendsStrict<{a: number; b: string}, {a: number}>>(true);
// Functions
expectType<ExtendsStrict<() => void, Function>>(true);
expectType<ExtendsStrict<Function, () => void>>(false);
expectType<ExtendsStrict<() => void, () => void>>(true);
expectType<ExtendsStrict<(...args: any[]) => unknown, Function>>(true);
// Intersections
expectType<ExtendsStrict<string & {bar: string}, string>>(true);
expectType<ExtendsStrict<string, string & {bar: string}>>(false);
// Literal vs primitive
expectType<ExtendsStrict<'foo', string>>(true);
expectType<ExtendsStrict<string, 'foo'>>(false);
// Arrays
expectType<ExtendsStrict<string[], string[]>>(true);
expectType<ExtendsStrict<[string], string[]>>(true); // Tuple is assignable to array
expectType<ExtendsStrict<string[], [string]>>(false); // Array not assignable to fixed tuple
// Branded types
type UserId = Tagged<string, 'UserId'>;
expectType<ExtendsStrict<UserId, string>>(true);
expectType<ExtendsStrict<string, UserId>>(false);
expectType<ExtendsStrict<UserId, UserId>>(true);
// Edge meta-types
expectType<ExtendsStrict<null, any>>(true);
expectType<ExtendsStrict<undefined, any>>(true);
expectType<ExtendsStrict<null, undefined>>(false);
expectType<ExtendsStrict<undefined, null>>(false);
expectType<ExtendsStrict<undefined, unknown>>(true);
expectType<ExtendsStrict<null, unknown>>(true);