Skip to content
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

Merged
merged 11 commits into from May 24, 2022
Merged
1 change: 1 addition & 0 deletions index.d.ts
Expand Up @@ -54,6 +54,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 @@ -122,6 +122,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);
}
}