From bf85819feca2943a77541a322f9a1c1feb07c2b0 Mon Sep 17 00:00:00 2001 From: George Zhao Date: Sun, 14 Jul 2024 08:18:49 +1000 Subject: [PATCH] `Exact`: Fix type when class is present (#911) Co-authored-by: Sindre Sorhus --- source/exact.d.ts | 23 +++++++++------ test-d/exact.ts | 72 ++++++++++++++++++++++++++++++++++++++++++++++- 2 files changed, 85 insertions(+), 10 deletions(-) diff --git a/source/exact.d.ts b/source/exact.d.ts index 3d8cdaf33..a6c5686b4 100644 --- a/source/exact.d.ts +++ b/source/exact.d.ts @@ -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`. @@ -52,11 +53,15 @@ onlyAcceptNameImproved(invalidInput); // Compilation error @category Utilities */ export type Exact = - IsEqual extends true ? ParameterType - // Convert union of array to array of union: A[] & B[] => (A & B)[] - : ParameterType extends unknown[] ? Array, ArrayElement>> - // In TypeScript, Array is a subtype of ReadonlyArray, so always test Array before ReadonlyArray. - : ParameterType extends readonly unknown[] ? ReadonlyArray, ArrayElement>> - // Only apply Exact for pure object types. For types from a class, leave it unchanged to TypeScript to handle. - : ParameterType extends JsonObject ? ExactObject - : 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 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 extends true ? ParameterType + // Convert union of array to array of union: A[] & B[] => (A & B)[] + : ParameterType extends unknown[] ? Array, ArrayElement>> + // In TypeScript, Array is a subtype of ReadonlyArray, so always test Array before ReadonlyArray. + : ParameterType extends readonly unknown[] ? ReadonlyArray, ArrayElement>> + : ExactObject; diff --git a/test-d/exact.ts b/test-d/exact.ts index d345f9f32..f74f8ad86 100644 --- a/test-d/exact.ts +++ b/test-d/exact.ts @@ -22,6 +22,28 @@ import type {Exact, Opaque} from '../index'; } } +{ // Spec - bigint type + type Type = bigint; + const function_ = >(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_ = >(arguments_: T) => arguments_; @@ -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_ = >(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); } } @@ -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_ = >(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); +}