-
-
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
Add Exact
type
#259
Add Exact
type
#259
Changes from 3 commits
563e785
3568ad7
cb77335
0efeae3
27d4303
902d046
f5174a3
8c90d29
d3172f5
926c9a8
2ed6d19
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 |
---|---|---|
@@ -0,0 +1,52 @@ | ||
import {Primitive} from './primitive'; | ||
import {KeysOfUnion} from './internal'; | ||
|
||
/** | ||
Create a type from type A and B and changes keys exclusive to type B to `never`. | ||
|
||
This is useful for function type-guarding to reject arguments with excess | ||
properties. Due to the nature of TypeScript, it does not complain if excess properties are | ||
provided unless the provided value is an object literal. | ||
|
||
@example | ||
``` | ||
type OnlyAcceptName = {name: string}; | ||
|
||
function onlyAcceptName(args: OnlyAcceptName) {} | ||
|
||
// TypeScript complains this because it's an object literal. | ||
onlyAcceptName({name: 'name', id: 1}); | ||
//=> `id` is excess | ||
|
||
// TypeScript does not complain because it's not an object literal. | ||
const invalidInput = {name: 'name', id: 1}; | ||
onlyAcceptName(invalidInput); // No errors | ||
``` | ||
|
||
Having `Exact` allows TypeScript to reject excess properties. | ||
|
||
@example | ||
``` | ||
import {Exact} from 'type-fest'; | ||
|
||
type OnlyAcceptName = {name: string}; | ||
|
||
function onlyAcceptNameImproved<T extends Exact<OnlyAcceptName, T>>(args: T) {} | ||
|
||
const invalidInput = {name: 'name', id: 1}; | ||
|
||
onlyAcceptNameImproved(invalidInput); // Compilation error | ||
``` | ||
|
||
The solution of `Exact` is | ||
- take both the preferred type and actual provided type as input. | ||
- generates the list of keys that exist in the provided type but not in the | ||
defined type. | ||
- mark these excess keys as `never` | ||
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. This should be an internal code comment, not in the doc comment. |
||
``` | ||
|
||
@category Utilities | ||
*/ | ||
export type Exact<ParameterType, InputType extends ParameterType> = ParameterType extends Primitive | ||
? ParameterType | ||
: {[Key in keyof ParameterType]: Exact<ParameterType[Key], InputType[Key]>} & Record<Exclude<keyof InputType, KeysOfUnion<ParameterType>>, never>; |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -42,3 +42,12 @@ export type Subtract<A extends number, B extends number> = BuildTuple<A> extends | |
Matches any primitive, `Date`, or `RegExp` value. | ||
*/ | ||
export type BuiltIns = Primitive | Date | RegExp; | ||
|
||
/** | ||
Returns the accessible keys that also works for union type. | ||
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. No idea what this is trying to say. But from reading the SO answer, I guess you meant something like this:
|
||
|
||
The reason a simple `keyof Union` does not work is because `keyof` always returns the accessible keys of a type. In the case of a union that will only be the common keys. | ||
|
||
@link https://stackoverflow.com/a/49402091 | ||
*/ | ||
export type KeysOfUnion<T> = T extends T ? keyof T : never; |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,145 @@ | ||
import {Exact} from '../index'; | ||
|
||
{ // Spec - string type | ||
type Type = string; | ||
const fn = <T extends Exact<Type, T>>(args: T) => args; | ||
|
||
{ // It should accept string | ||
const input = ''; | ||
fn(input); | ||
} | ||
|
||
{ // It should reject number | ||
const input = 1; | ||
// @ts-expect-error | ||
fn(input); | ||
} | ||
|
||
{ // It should reject object | ||
const input = {}; | ||
// @ts-expect-error | ||
fn(input); | ||
} | ||
} | ||
|
||
{ // Spec - array | ||
type Type = Array<{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 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 - object | ||
type Type = {code: string; name?: string}; | ||
const fn = <T extends Exact<Type, T>>(args: T) => args; | ||
|
||
{ // It should accept object with required property only | ||
const input = {code: ''}; | ||
fn(input); | ||
} | ||
|
||
{ // It should accept object with optional property | ||
const input = {code: '', name: ''}; | ||
fn(input); | ||
} | ||
|
||
{ // It should reject object 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 - union - only object | ||
type Type = {code: string} | {name: string}; | ||
const fn = <T extends Exact<Type, T>>(args: T) => args; | ||
|
||
{ // It should accept type a | ||
const input = {code: ''}; | ||
fn(input); | ||
} | ||
|
||
{ // It should accept type b | ||
const input = {name: ''}; | ||
fn(input); | ||
} | ||
|
||
{ // It should reject intersection | ||
const input = {name: '', code: ''}; | ||
// @ts-expect-error | ||
fn(input); | ||
} | ||
} | ||
|
||
{ // Spec - union - mixture object/primitive | ||
type Type = {code: string} | string; | ||
const fn = <T extends Exact<Type, T>>(args: T) => args; | ||
|
||
{ // It should accept type a | ||
const input = {code: ''}; | ||
fn(input); | ||
} | ||
|
||
{ // It should accept type b | ||
const input = ''; | ||
fn(input); | ||
} | ||
|
||
{ // It should reject intersection | ||
const input = {name: '', code: ''}; | ||
// @ts-expect-error | ||
fn(input); | ||
} | ||
} | ||
|
||
{ // Spec - jsonschema2ts generated request type with additionalProperties: true | ||
type Type = { | ||
body: { | ||
[k: string]: unknown; | ||
code: string; | ||
name?: string; | ||
}; | ||
}; | ||
const fn = <T extends Exact<Type, T>>(args: T) => args; | ||
|
||
{ // It should accept input with required property only | ||
const input = {body: {code: ''}}; | ||
fn(input); | ||
} | ||
|
||
{ // It should accept input with optional property | ||
const input = {body: {code: '', name: ''}}; | ||
fn(input); | ||
} | ||
|
||
{ // It should allow input with excess property | ||
const input = {body: {code: '', name: '', excessProperty: ''}}; | ||
fn(input); | ||
} | ||
} |
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.
Typo
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.
I am not sure which word has typo but I rewrite this sentence and hopefully this makes more sense.
I have updated the other comments as suggested.
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.
I would recommend using a grammar checker like Grammarly. It would have caught this.