Skip to content

Commit

Permalink
Exact: Fix type when class is present (#911)
Browse files Browse the repository at this point in the history
Co-authored-by: Sindre Sorhus <[email protected]>
  • Loading branch information
zorji and sindresorhus committed Jul 13, 2024
1 parent fee4e04 commit bf85819
Show file tree
Hide file tree
Showing 2 changed files with 85 additions and 10 deletions.
23 changes: 14 additions & 9 deletions source/exact.d.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import type {ArrayElement, ObjectValue} from './internal';
import type {IsEqual} from './is-equal';
import type {KeysOfUnion} from './keys-of-union';
import type {JsonObject} from './basic';
import type {IsUnknown} from './is-unknown';
import type {Primitive} from './primitive';

/**
Create a type from `ParameterType` and `InputType` and change keys exclusive to `InputType` to `never`.
Expand Down Expand Up @@ -52,11 +53,15 @@ onlyAcceptNameImproved(invalidInput); // Compilation error
@category Utilities
*/
export type Exact<ParameterType, InputType> =
IsEqual<ParameterType, InputType> extends true ? ParameterType
// 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>>>
// Only apply Exact for pure object types. For types from a class, leave it unchanged to TypeScript to handle.
: ParameterType extends JsonObject ? ExactObject<ParameterType, InputType>
: ParameterType;
// If the parameter is a primitive, return it as is immediately to avoid it being converted to a complex type
ParameterType extends Primitive ? ParameterType
// If the parameter is an unknown, return it as is immediately to avoid it being converted to a complex type
: IsUnknown<ParameterType> extends true ? unknown
// If the parameter is a Function, return it as is because this type is not capable of handling function, leave it to TypeScript
: ParameterType extends Function ? ParameterType
: IsEqual<ParameterType, InputType> extends true ? ParameterType
// 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>>>
: ExactObject<ParameterType, InputType>;
72 changes: 71 additions & 1 deletion test-d/exact.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,28 @@ import type {Exact, Opaque} from '../index';
}
}

{ // Spec - bigint type
type Type = bigint;
const function_ = <T extends Exact<Type, T>>(arguments_: T) => arguments_;

{ // It should accept bigint
const input = BigInt(9_007_199_254_740_991);
function_(input);
}

{ // It should reject number
const input = 1;
// @ts-expect-error
function_(input);
}

{ // It should reject unknown
const input = {} as unknown;
// @ts-expect-error
function_(input);
}
}

{ // Spec - array
type Type = Array<{code: string; name?: string}>;
const function_ = <T extends Exact<Type, T>>(arguments_: T) => arguments_;
Expand Down Expand Up @@ -195,7 +217,25 @@ import type {Exact, Opaque} from '../index';
}

{ // It should allow input with excess property
const input = {body: {code: '', name: '', excessProperty: ''}};
const input = {body: {code: '', name: '', excessProperty: 1}};
function_(input);
}
}

{ // Spec - check index signature type
type Type = {
body: {
[k: string]: string;
code: string;
name?: string;
};
};
const function_ = <T extends Exact<Type, T>>(arguments_: T) => arguments_;

{ // It should allow input with an excess property
const input = {body: {code: '', name: '', excessProperty: 1}};
// Expects error because the excess property is not string
// @ts-expect-error
function_(input);
}
}
Expand Down Expand Up @@ -467,3 +507,33 @@ import type {Exact, Opaque} from '../index';
// @ts-expect-error
function_({a: 'a', b: 1});
}

// Spec - special test case for Date type
// @see https://github.com/sindresorhus/type-fest/issues/909
{
type UserType = {
id: string;
name: string;
createdAt: Date;
email?: string;
};

const function_ = <T extends Exact<UserType, T>>(arguments_: T) => arguments_;

function_({
id: 'asd',
name: 'John',
createdAt: new Date(),
});

const withExcessSurname = {
id: 'asd',
name: 'John',
createdAt: new Date(),
surname: 'Doe',
};

// Expects error due to surname is an excess field
// @ts-expect-error
function_(withExcessSurname);
}

0 comments on commit bf85819

Please sign in to comment.