Skip to content

Commit

Permalink
Except: Add requireExactProps option (#560)
Browse files Browse the repository at this point in the history
Co-authored-by: Sindre Sorhus <sindresorhus@gmail.com>
Co-authored-by: Tommy <tmitchell7@uh.edu>
  • Loading branch information
3 people committed Mar 23, 2023
1 parent 18f8a11 commit c5743c9
Show file tree
Hide file tree
Showing 3 changed files with 49 additions and 7 deletions.
5 changes: 5 additions & 0 deletions package.json
Expand Up @@ -49,5 +49,10 @@
"@typescript-eslint/no-redeclare": "off",
"@typescript-eslint/no-confusing-void-expression": "off"
}
},
"tsd": {
"compilerOptions": {
"noUnusedLocals": false
}
}
}
33 changes: 28 additions & 5 deletions source/except.d.ts
Expand Up @@ -29,9 +29,22 @@ type Filtered = Filter<'bar', 'foo'>;
*/
type Filter<KeyType, ExcludeType> = IsEqual<KeyType, ExcludeType> extends true ? never : (KeyType extends ExcludeType ? never : KeyType);

type ExceptOptions = {
/**
Disallow assigning non-specified properties.
Note that any omitted properties in the resulting type will be present in autocomplete as `undefined`.
@default false
*/
requireExactProps?: boolean;
};

/**
Create a type from an object type without certain keys.
We recommend setting the `requireExactProps` option to `true`.
This type is a stricter version of [`Omit`](https://www.typescriptlang.org/docs/handbook/release-notes/typescript-3-5.html#the-omit-helper-type). The `Omit` type does not restrict the omitted keys to be keys present on the given type, while `Except` does. The benefits of a stricter type are avoiding typos and allowing the compiler to pick up on rename refactors automatically.
This type was proposed to the TypeScript team, which declined it, saying they prefer that libraries implement stricter versions of the built-in types ([microsoft/TypeScript#30825](https://github.com/microsoft/TypeScript/issues/30825#issuecomment-523668235)).
Expand All @@ -43,15 +56,25 @@ import type {Except} from 'type-fest';
type Foo = {
a: number;
b: string;
c: boolean;
};
type FooWithoutA = Except<Foo, 'a' | 'c'>;
//=> {b: string};
type FooWithoutA = Except<Foo, 'a'>;
//=> {b: string}
const fooWithoutA: FooWithoutA = {a: 1, b: '2'};
//=> errors: 'a' does not exist in type '{ b: string; }'
type FooWithoutB = Except<Foo, 'b', {requireExactProps: true}>;
//=> {a: number} & Partial<Record<"b", never>>
const fooWithoutB: FooWithoutB = {a: 1, b: '2'};
//=> errors at 'b': Type 'string' is not assignable to type 'undefined'.
```
@category Object
*/
export type Except<ObjectType, KeysType extends keyof ObjectType> = {
export type Except<ObjectType, KeysType extends keyof ObjectType, Options extends ExceptOptions = {requireExactProps: false}> = {
[KeyType in keyof ObjectType as Filter<KeyType, KeysType>]: ObjectType[KeyType];
};
} & (Options['requireExactProps'] extends true
? Partial<Record<KeysType, never>>
: {});
18 changes: 16 additions & 2 deletions test-d/except.ts
@@ -1,8 +1,22 @@
import {expectType} from 'tsd';
import {expectType, expectError} from 'tsd';
import type {Except} from '../index';

declare const except: Except<{a: number; b: string}, 'b'>;
expectType<{a: number}>(except);
expectError(except.b);

const nonStrict = {
a: 1,
b: '2',
};

const nonStrictAssignment: typeof except = nonStrict; // No error

declare const strictExcept: Except<{a: number; b: string}, 'b', {requireExactProps: true}>;

expectError(() => {
const strictAssignment: typeof strictExcept = nonStrict;
});

// Generic properties
type Example = {
Expand All @@ -11,7 +25,7 @@ type Example = {
bar: string;
};

const test: Except<Example, 'bar'> = {foo: 123, bar: 'asdf'};
const test: Except<Example, 'bar', {requireExactProps: false}> = {foo: 123, bar: 'asdf'};
expectType<number>(test.foo);
// eslint-disable-next-line @typescript-eslint/dot-notation
expectType<unknown>(test['bar']);

0 comments on commit c5743c9

Please sign in to comment.