Skip to content

Commit

Permalink
Add Exact type (#259)
Browse files Browse the repository at this point in the history
Co-authored-by: Sindre Sorhus <sindresorhus@gmail.com>
  • Loading branch information
zorji and sindresorhus committed May 24, 2022
1 parent f2aae51 commit 9394d54
Show file tree
Hide file tree
Showing 5 changed files with 207 additions and 0 deletions.
1 change: 1 addition & 0 deletions index.d.ts
Expand Up @@ -55,6 +55,7 @@ export {
NonNegativeInteger,
} from './source/numeric';
export {StringKeyOf} from './source/string-key-of';
export {Exact} from './source/exact';

// Template literal types
export {CamelCase} from './source/camel-case';
Expand Down
1 change: 1 addition & 0 deletions readme.md
Expand Up @@ -190,6 +190,7 @@ Click the type names for complete docs.
- [`Get`](source/get.d.ts) - Get a deeply-nested property from an object using a key path, like [Lodash's `.get()`](https://lodash.com/docs/latest#get) function.
- [`StringKeyOf`](source/string-key-of.d.ts) - Get keys of the given type as strings.
- [`Schema`](source/schema.d.ts) - Create a deep version of another object type where property values are recursively replaced into a given value type.
- [`Exact`](source/exact.d.ts) - Create a type that does not allow extra properties.

### JSON

Expand Down
51 changes: 51 additions & 0 deletions source/exact.d.ts
@@ -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>;
9 changes: 9 additions & 0 deletions source/internal.d.ts
Expand Up @@ -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;

/**
Gets keys from a type. Similar to `keyof` but this one also works for union types.
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;
145 changes: 145 additions & 0 deletions test-d/exact.ts
@@ -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);
}
}

0 comments on commit 9394d54

Please sign in to comment.