From 800bec3edd534f80b4f168d6e627881433450e35 Mon Sep 17 00:00:00 2001 From: Nigel Thomas Roy Date: Fri, 2 May 2025 21:19:32 -0400 Subject: [PATCH] feat:add ExtractStrict improved built-in --- index.d.ts | 3 ++ readme.md | 4 +++ source/extract-strict.d.ts | 56 ++++++++++++++++++++++++++++++++ test-d/extract-strict.ts | 66 ++++++++++++++++++++++++++++++++++++++ 4 files changed, 129 insertions(+) create mode 100644 source/extract-strict.d.ts create mode 100644 test-d/extract-strict.ts diff --git a/index.d.ts b/index.d.ts index f03837c6a..b66e57a7e 100644 --- a/index.d.ts +++ b/index.d.ts @@ -175,3 +175,6 @@ export type {LastArrayElement} from './source/last-array-element'; export type {GlobalThis} from './source/global-this'; export type {PackageJson} from './source/package-json'; export type {TsConfigJson} from './source/tsconfig-json'; + +// Improved Built-in +export type {ExtractStrict} from './source/extract-strict'; diff --git a/readme.md b/readme.md index baa648a24..9684c3be5 100644 --- a/readme.md +++ b/readme.md @@ -332,6 +332,10 @@ type ShouldBeNever = IfAny<'not any', 'not never', 'never'>; - [`PackageJson`](source/package-json.d.ts) - Type for [npm's `package.json` file](https://docs.npmjs.com/creating-a-package-json-file). It also includes support for [TypeScript Declaration Files](https://www.typescriptlang.org/docs/handbook/declaration-files/publishing.html). - [`TsConfigJson`](source/tsconfig-json.d.ts) - Type for [TypeScript's `tsconfig.json` file](https://www.typescriptlang.org/docs/handbook/tsconfig-json.html). +### Improved Built-in + +- [`ExtractStrict`](source/extract-strict.d.ts) - Like `Extract`, but all members of `Union` are restricted to be subsets of some member of `Type`. + ## Declined types *If we decline a type addition, we will make sure to document the better solution here.* diff --git a/source/extract-strict.d.ts b/source/extract-strict.d.ts new file mode 100644 index 000000000..be25bff85 --- /dev/null +++ b/source/extract-strict.d.ts @@ -0,0 +1,56 @@ +import type {Exact} from './exact'; + +/** +Extract members of a union type `Type` based on the +fields in the given union type `Union`, where each +union member of `Union` is only allowed to be a subset +of some union member of `Type`. + +Constraint: ∀ U ∈ Union, U ⊆ T, where T ∈ Type + +@example +``` +type Foo = { + kind: 'foo'; + a: string; + b: string; +}; + +type Bar = { + kind: 'bar'; + a: string; + b: number; + c: boolean; +}; + +type Foobar = Foo | Bar; + +type FoobarByA = ExtractStrict; +// => Foobar + +type OnlyFooByKind = ExtractStrict; +// => Foo + +type OnlyFooByB = ExtractStrict; +// => Foo + +type OnlyBarByC = ExtractStrict; +// => Bar + +type InvalidUnionForType = ExtractStrict; +// => Error: +// Types of property 'd' are incompatible. +// Type 'string' is not assignable to type 'never'. +``` +@category Improved Builtin +*/ +export type ExtractStrict< + Type, + /** + * Only allow keys that are in some union member + * of `Type`. Thus, each union member of `Union` + * is only allowed to be a subset of some union + * member of `Type`. + */ + Union extends Partial>, +> = Extract; diff --git a/test-d/extract-strict.ts b/test-d/extract-strict.ts new file mode 100644 index 000000000..f9ccf71a0 --- /dev/null +++ b/test-d/extract-strict.ts @@ -0,0 +1,66 @@ +import {expectType} from 'tsd'; +import type {ExtractStrict} from '../source/extract-strict'; + +// Primitive union tests + +type ShirtSize = 'xxxl' | 'xxl' | 'xl' | 'l' | 'm' | 's' | 'xs' | 'xxs'; +type LargeShirtSize = 'xxxl' | 'xxl' | 'xl' | 'l'; +type SmallShirtSize = 's' | 'xs' | 'xxs'; + +declare const largeShirtSizes: ExtractStrict; +expectType(largeShirtSizes); + +declare const smallShirtSizes: ExtractStrict; +expectType(smallShirtSizes); + +// @ts-expect-error +declare const allInvalidShirtSizes: ExtractStrict; +expectType(allInvalidShirtSizes); + +// @ts-expect-error +declare const someInvalidShirtSizes: ExtractStrict; +expectType<'m'>(someInvalidShirtSizes); // This is how native `Extract` works with primitives + +// Object union tests + +type Foo = { + kind: 'foo'; + a: string; + b: string; +}; + +type Bar = { + kind: 'bar'; + a: string; + b: number; + c: boolean; +}; + +type Foobar = Foo | Bar; + +declare const foobarByA: ExtractStrict; +expectType(foobarByA); + +declare const onlyFooByKind: ExtractStrict; +expectType(onlyFooByKind); + +declare const onlyFooByB: ExtractStrict; +expectType(onlyFooByB); + +declare const onlyBarByC: ExtractStrict; +expectType(onlyBarByC); + +declare const foobarByUnionBC: ExtractStrict; +expectType(foobarByUnionBC); + +// @ts-expect-error +declare const invalidLoneField: ExtractStrict; +expectType(invalidLoneField); + +// @ts-expect-error +declare const invalidMixedFields: ExtractStrict; +expectType(invalidMixedFields); + +// @ts-expect-error +declare const undefinedField: ExtractStrict; +expectType(undefinedField);