Skip to content

Commit

Permalink
Exact: Support array union (#421)
Browse files Browse the repository at this point in the history
Co-authored-by: Sindre Sorhus <sindresorhus@gmail.com>
  • Loading branch information
zorji and sindresorhus committed Aug 22, 2022
1 parent 290e3a2 commit edcad04
Show file tree
Hide file tree
Showing 2 changed files with 222 additions and 9 deletions.
40 changes: 31 additions & 9 deletions source/exact.d.ts
@@ -1,6 +1,29 @@
import type {Primitive} from './primitive';
import type {KeysOfUnion} from './internal';

/**
Extract the element of an array that also works for array union.
Returns `never` if T is not an array.
It creates a type-safe way to access the element type of `unknown` type.
*/
type ArrayElement<T> = T extends readonly unknown[] ? T[0] : never;

/**
Extract the object field type if T is an object and K is a key of T, return `never` otherwise.
It creates a type-safe way to access the member type of `unknown` type.
*/
type ObjectValue<T, K> = K extends keyof T ? T[K] : never;

/**
Create a type from `ParameterType` and `InputType` and change keys exclusive to `InputType` to `never`.
- Generate a list of keys that exists in `InputType` but not in `ParameterType`.
- Mark these excess keys as `never`.
*/
type ExactObject<ParameterType, InputType> = {[Key in keyof ParameterType]: Exact<ParameterType[Key], ObjectValue<InputType, Key>>}
& Record<Exclude<keyof InputType, KeysOfUnion<ParameterType>>, never>;

/**
Create a type that does not allow extra properties, meaning it only allows properties that are explicitly declared.
Expand Down Expand Up @@ -41,11 +64,10 @@ onlyAcceptNameImproved(invalidInput); // Compilation error
@category Utilities
*/
export type Exact<ParameterType, InputType extends ParameterType> = ParameterType extends Primitive
? ParameterType
/*
Create a type from `ParameterType` and `InputType` and change keys exclusive to `InputType` to `never`.
- Generate a list of keys that exists in `InputType` but not in `ParameterType`.
- Mark these excess keys as `never`.
*/
: {[Key in keyof ParameterType]: Exact<ParameterType[Key], InputType[Key]>} & Record<Exclude<keyof InputType, KeysOfUnion<ParameterType>>, never>;
export type Exact<ParameterType, InputType> =
// Convert union of array to array of union: A[] & B[] => (A & B)[]
ParameterType extends unknown[] ? Array<Exact<ArrayElement<ParameterType>, ArrayElement<InputType>>>
// In TypeScript, Array is a subtype of ReadonlyArray, so always test Array before ReadonlyArray.
: ParameterType extends readonly unknown[] ? ReadonlyArray<Exact<ArrayElement<ParameterType>, ArrayElement<InputType>>>
: ParameterType extends object ? ExactObject<ParameterType, InputType>
: ParameterType;
191 changes: 191 additions & 0 deletions test-d/exact.ts
Expand Up @@ -31,6 +31,44 @@ import type {Exact} from '../index';
fn(input);
}

{ // It should reject readonly array
const input = [{code: ''}] as ReadonlyArray<{code: string}>;
// @ts-expect-error
fn(input);
}

{ // It should accept array with optional property
const input = [{code: '', name: ''}];
fn(input);
}

{ // It should reject array with excess property
const input = [{code: '', name: '', excessProperty: ''}];
// @ts-expect-error
fn(input);
}

{ // It should reject invalid type
const input = '';
// @ts-expect-error
fn(input);
}
}

{ // Spec - readonly array
type Type = ReadonlyArray<{code: string; name?: string}>;
const fn = <T extends Exact<Type, T>>(args: T) => args;

{ // It should accept array with required property only
const input = [{code: ''}];
fn(input);
}

{ // It should accept readonly array
const input = [{code: ''}] as ReadonlyArray<{code: string}>;
fn(input);
}

{ // It should accept array with optional property
const input = [{code: '', name: ''}];
fn(input);
Expand Down Expand Up @@ -143,3 +181,156 @@ import type {Exact} from '../index';
fn(input);
}
}

{ // Spec - union of array
type Type = Array<{x: string}> & Array<{z: number}>;
const fn = <T extends Exact<Type, T>>(args: T) => args;

{ // It should accept valid input
const input = [{
x: '',
z: 1,
}];
fn(input);
}

{ // It should reject missing field
const input = [{
z: 1,
}];
// @ts-expect-error
fn(input);
}

{ // It should reject missing field
const input = [{
x: '',
}];
// @ts-expect-error
fn(input);
}

{ // It should reject incorrect type
const input = [{
x: 1,
z: 1,
}];
// @ts-expect-error
fn(input);
}

{ // It should reject excess field
const input = [{
x: '',
y: '',
z: 1,
}];
// @ts-expect-error
fn(input);
}
}

{ // Spec - union of readonly array + non readonly array
type Type = ReadonlyArray<{x: string}> & Array<{z: number}>;
const fn = <T extends Exact<Type, T>>(args: T) => args;

{ // It should accept valid input
const input = [{
x: '',
z: 1,
}];
fn(input);
}

{ // It should reject missing field
const input = [{
z: 1,
}];
// @ts-expect-error
fn(input);
}

{ // It should reject missing field
const input = [{
x: '',
}];
// @ts-expect-error
fn(input);
}

{ // It should reject incorrect type
const input = [{
x: 1,
z: 1,
}];
// @ts-expect-error
fn(input);
}

{ // It should reject excess field
const input = [{
x: '',
y: '',
z: 1,
}];
// @ts-expect-error
fn(input);
}
}

{ // Spec - union of array with nested fields
type Type = Array<{x: string}> & Array<{z: number; d: {e: string; f: boolean}}>;
const fn = <T extends Exact<Type, T>>(args: T) => args;

{ // It should accept valid input
const input = [{
x: '',
z: 1,
d: {
e: 'test',
f: true,
},
}];
fn(input);
}

{ // It should reject excess field
const input = [{
x: '',
z: 1,
d: {
e: 'test',
f: true,
g: '', // Excess field
},
}];
// @ts-expect-error
fn(input);
}

{ // It should reject missing field
const input = [{
x: '',
z: 1,
d: {
e: 'test',
// Missing f: boolean
},
}];
// @ts-expect-error
fn(input);
}

{ // It should reject missing field
const input = [{
x: '',
z: 1,
d: {
e: 'test',
f: '', // Type mismatch
},
}];
// @ts-expect-error
fn(input);
}
}

0 comments on commit edcad04

Please sign in to comment.