Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Co-authored-by: Sindre Sorhus <sindresorhus@gmail.com>
- Loading branch information
1 parent
f2aae51
commit 9394d54
Showing
5 changed files
with
207 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,51 @@ | ||
import type {Primitive} from './primitive'; | ||
import type {KeysOfUnion} from './internal'; | ||
|
||
/** | ||
Create a type that does not allow extra properties, meaning it only allows properties that are explicitly declared. | ||
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. | ||
*Please upvote [this issue](https://github.com/microsoft/TypeScript/issues/12936) if you want to have this type as a built-in in TypeScript.* | ||
@example | ||
``` | ||
type OnlyAcceptName = {name: string}; | ||
function onlyAcceptName(args: OnlyAcceptName) {} | ||
// TypeScript complains about excess properties when an object literal is provided. | ||
onlyAcceptName({name: 'name', id: 1}); | ||
//=> `id` is excess | ||
// TypeScript does not complain about excess properties when the provided value is a variable (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 | ||
``` | ||
[Read more](https://stackoverflow.com/questions/49580725/is-it-possible-to-restrict-typescript-object-to-contain-only-properties-defined) | ||
@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>; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,145 @@ | ||
import type {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); | ||
} | ||
} |