-
-
Notifications
You must be signed in to change notification settings - Fork 502
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
fix(Exact): support array union #421
Changes from 2 commits
72d6c6f
97d3dc8
886c4fb
87307fd
b4c76e6
8ddbc11
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,6 +1,26 @@ | ||
import type {Primitive} from './primitive'; | ||
import type {KeysOfUnion} from './internal'; | ||
|
||
/** | ||
Extract the element of an array that also works for array union. | ||
Return `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; | ||
zorji marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
/** | ||
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. | ||
|
||
|
@@ -41,11 +61,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> = | ||
// Converting union of array to array of union. i.e. A[] & B[] => (A & B)[] | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. |
||
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; |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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); | ||
|
@@ -143,3 +181,156 @@ import type {Exact} from '../index'; | |
fn(input); | ||
} | ||
} | ||
|
||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. A little bit out of the scope of this PR but can we also include exhaustive tests against ReadonlyArray as well? I just want to ensure ReadonlyArray will work consistently in the future along with the regular mutable arrays. We can just copy & paste Array spec for the ReadonlyArray test. I just quickly tested Array spec against ReadonlyArray and it seems to work fine |
||
{ // 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); | ||
} | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Possibly we can include more test coverages to check excess, exact, missing and mismatching fields for the union array test |
||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Could this use the trick mentioned in #118 (comment) ?