diff --git a/source/exact.d.ts b/source/exact.d.ts index e1806fde9..0227f3d04 100644 --- a/source/exact.d.ts +++ b/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 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 = 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 = {[Key in keyof ParameterType]: Exact>} + & Record>, never>; + /** Create a type that does not allow extra properties, meaning it only allows properties that are explicitly declared. @@ -41,11 +64,10 @@ onlyAcceptNameImproved(invalidInput); // Compilation error @category Utilities */ -export type Exact = 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} & Record>, never>; +export type Exact = + // 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>> + : ParameterType extends object ? ExactObject + : ParameterType; diff --git a/test-d/exact.ts b/test-d/exact.ts index 101e0adf4..42ac76520 100644 --- a/test-d/exact.ts +++ b/test-d/exact.ts @@ -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 = >(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); @@ -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 = >(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 = >(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 = >(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); + } +}