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

Add Exact type #259

merged 11 commits into from May 24, 2022

Conversation

zorji
Copy link
Contributor

@zorji zorji commented Sep 8, 2021

Exact is a type to allow a function to reject excess properties.

TypeScript only performs excess property check on object literal only. (see explanation https://stackoverflow.com/a/62358260)

i.e. it does not complain when you assign an object that has more fields than the required type except when it's an object literal.

Here is the demo code

type GuardType = {
  allowedField: string
}

type InputType = {
  excessField: string
  allowedField: string
}

const fn = (attr: GuardType) => {
  return attr
}

const aWithType: TypeA = {
  excessField: '',
  allowedField: '',
}

const aWithoutType = {
  excessField: '',
  allowedField: '',
}

// no complains
fn(aWithType)

// no complains
fn({
  ...aWithType,
})

// complains when it's an object literal
// complains excess field is not accepted
fn({
  name: 'name',
  code: 'code',
})

// no complains
fn(aWithoutType)

To have a better type check, we can apply this Exact<T> to the above demo (inspired from https://stackoverflow.com/a/59230299)

My implementation is a deep Exactly and also use KeysOfUnion from https://stackoverflow.com/a/49402091

type GuardType = {
  allowedField: string
}

type InputType = {
  excessField: string
  allowedField: string
}

const aWithType: InputType = {
  excessField: '',
  allowedField: '',
}

const aWithoutType = {
  excessField: '',
  allowedField: '',
}

const fn2 = <T extends Exact<GuardType, T>>(attr: T) => {
  return attr
}

// complains excess field is not accepted
fn2(aWithType)

// complains excess field is not accepted
fn2({
  ...aWithType,
})

// complains excess field is not accepted
fn2({
  excessField: '',
  allowedField: '',
})

// complains excess field is not accepted
fn2(aWithoutType)

The idea is to find all excess properties and mark them as never.

@zorji zorji changed the title Add TypeName type Add Exactly type Sep 8, 2021
@sindresorhus
Copy link
Owner

@sindresorhus
Copy link
Owner

I think Exact would be a better name.

@sindresorhus
Copy link
Owner

The type description and docs text needs more work.

readme.md Outdated
@@ -124,6 +124,7 @@ Click the type names for complete docs.
- [`Includes`](source/includes.d.ts) - Returns a boolean for whether the given array includes the given item.
- [`Simplify`](source/simplify.d.ts) - Flatten the type output to improve type hints shown in editors.
- [`Jsonify`](source/jsonify.d.ts) - Transform a type to one that is assignable to the `JsonValue` type.
- [`Exactly`](source/exactly.d.ts) - Create a guard for function to reject excess properties.
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The type is not just about guarding functions.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I can only think of guarding a function because it requires input type from a function input. Could you suggest other usages?

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The descriptions describes one of the uses of the type, but the description should describe what it does.

@@ -0,0 +1,45 @@
import {Primitive} from './primitive';

type KeysOfUnion<T> = T extends T ? keyof T : never;
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Needs a short description.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

description added.

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This type could be moved into internal.d.ts as it could be useful for other types here too.

source/exactly.d.ts Outdated Show resolved Hide resolved
source/exactly.d.ts Outdated Show resolved Hide resolved
source/exactly.d.ts Outdated Show resolved Hide resolved
@sindresorhus
Copy link
Owner

Looks like this partly fixes: #156

@zorji
Copy link
Contributor Author

zorji commented Sep 30, 2021

Thanks @sindresorhus

I'll review the other implementation in the SO link u suggested.

@zorji
Copy link
Contributor Author

zorji commented Nov 7, 2021

Progress

Reviewing another Exact implementation from dotansimha/graphql-code-generator#4194 but looks like it doesn't solve the issue.

@zorji
Copy link
Contributor Author

zorji commented Nov 7, 2021

Have you seen https://stackoverflow.com/questions/49580725/is-it-possible-to-restrict-typescript-object-to-contain-only-properties-defined ?

Hi @sindresorhus sorry I finally have some time to pick up the work on this again.

I have reviewed the solution in this SO post and surprisingly the concept is exactly the same as my solution in this PR.

So the solution is basically "Find all excess properties from the input type and mark them as never."

Are you suggesting you prefer to use the name from the SO post above?

@zorji
Copy link
Contributor Author

zorji commented Nov 7, 2021

Sounds like a useful type.

Please follow: https://github.com/sindresorhus/type-fest/blob/main/.github/contributing.md#submitting-a-new-type

Hi @sindresorhus

I have tried to rewrite tests with expectError from tsd and functions from expect-type. However, I found that both the tests using tsd and expect-type can perform better tests and explain the intention better, therefore I feel it's more maintainable to keep the tests in the PR.

I have also verified that the tests are executing correctly when running npx tsd. e.g. if I remove this line here it does trigger an error.

Do you help review and see what else should be improved? Much appreciated.

@zorji zorji changed the title Add Exactly type Add Exact type Nov 7, 2021
@sindresorhus
Copy link
Owner

Are you suggesting you prefer to use the name from the SO post above?

No, Exact should be the name.

@sindresorhus
Copy link
Owner

Please help review the other open pull requests. If there are no open pull requests, provide some feedback on some of the open issues. - https://github.com/sindresorhus/type-fest/blob/main/.github/contributing.md#submitting-a-new-type

readme.md Outdated
@@ -124,6 +124,7 @@ Click the type names for complete docs.
- [`Includes`](source/includes.d.ts) - Returns a boolean for whether the given array includes the given item.
- [`Simplify`](source/simplify.d.ts) - Flatten the type output to improve type hints shown in editors.
- [`Jsonify`](source/jsonify.d.ts) - Transform a type to one that is assignable to the `JsonValue` type.
- [`Exact`](source/exact.d.ts) - Create a type from type A and B and convert all keys exclusive to type B as `never`.
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the code review @sindresorhus
I have updated the description to describe what it does instead of what it can do.

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You are now describing the actual logic of the code. The "what it does" needs to be more high level. Look at the other descriptions for inspiration.

@zorji
Copy link
Contributor Author

zorji commented Nov 10, 2021

Please help review the other open pull requests. If there are no open pull requests, provide some feedback on some of the open issues. - https://github.com/sindresorhus/type-fest/blob/main/.github/contributing.md#submitting-a-new-type

I'll try my best. Currently trying to review #156

@@ -0,0 +1,45 @@
import {Primitive} from './primitive';

type KeysOfUnion<T> = T extends T ? keyof T : never;
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This type could be moved into internal.d.ts as it could be useful for other types here too.

Create a type from type A and B and convert all keys exclusive to type B as `never`.

This is useful when function wants to guard the arguments to only accept the
exact defined keys and reject any excess properties.
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Don't hard-wrap.

readme.md Outdated
@@ -124,6 +124,7 @@ Click the type names for complete docs.
- [`Includes`](source/includes.d.ts) - Returns a boolean for whether the given array includes the given item.
- [`Simplify`](source/simplify.d.ts) - Flatten the type output to improve type hints shown in editors.
- [`Jsonify`](source/jsonify.d.ts) - Transform a type to one that is assignable to the `JsonValue` type.
- [`Exact`](source/exact.d.ts) - Create a type from type A and B and convert all keys exclusive to type B as `never`.
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You are now describing the actual logic of the code. The "what it does" needs to be more high level. Look at the other descriptions for inspiration.

source/exact.d.ts Outdated Show resolved Hide resolved
source/exact.d.ts Outdated Show resolved Hide resolved
source/exact.d.ts Outdated Show resolved Hide resolved
@sindresorhus
Copy link
Owner

Friendly bump

Exact creates 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
@zorji zorji requested a review from sindresorhus March 7, 2022 12:32
@gulsharan
Copy link

@sindresorhus can you give an idea when this will be merged/released?

@sindresorhus
Copy link
Owner

can you give an idea when this will be merged/released?

No, but if you want to see it released, you can help review.

@sindresorhus
Copy link
Owner

I do intend to merge this, but it still requires many docs improvements, and I don't have to do them right now.

@zorji
Copy link
Contributor Author

zorji commented Mar 10, 2022

I do intend to merge this, but it still requires many docs improvements, and I don't have to do them right now.

Hi @sindresorhus do you mind help point out which doc i can improve on, i will do it asap.

e.g.

  • is it the doc missing more examples
  • or missing something to explain how it works

the following comments have been addressed but if it still requires more improvement, pls let me know

  • You are now describing the actual logic of the code. The "what it does" needs to be more high level. Look at the other descriptions for inspiration.
  • This type could be moved into internal.d.ts as it could be useful for other types here too.
  • The descriptions describes one of the uses of the type, but the description should describe what it does.

@sindresorhus
Copy link
Owner

You are now describing the actual logic of the code. The "what it does" needs to be more high level. Look at the other descriptions for inspiration.

You are still describing the code. I think the Stack Overflow answers describes it more clearly:

which properties are restricted only to those explicitly declared

- 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`
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should be an internal code comment, not in the doc comment.

@@ -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.
Copy link
Owner

Choose a reason for hiding this comment

The 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:

Get all the keys from types in a union type.


function onlyAcceptName(args: OnlyAcceptName) {}

// TypeScript complains this because it's an object literal.
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Typo

Copy link
Contributor Author

@zorji zorji Mar 19, 2022

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.

Copy link
Owner

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.

@sindresorhus
Copy link
Owner

I finally had time to look at this.

Thanks ;)

@sindresorhus sindresorhus merged commit 9394d54 into sindresorhus:main May 24, 2022
@adam-thomas-privitar
Copy link

adam-thomas-privitar commented Jun 6, 2022

I think theres a gap in this implementation where it wont search for the exactness of types in arrays. Something like this might work

export type Exact<ParameterType, InputType extends ParameterType> = ParameterType extends Primitive
  ? ParameterType
  : ParameterType extends Array<infer U>
  ? InputType extends Array<any>
    ? Array<Exact<U, InputType[0]>>
    : never
  : { [Key in keyof ParameterType]: Exact<ParameterType[Key], InputType[Key]> } & Record<
      Exclude<keyof InputType, KeysOfUnion<ParameterType>>,
      never
    >

But I don't know if this is 100% correct

@zorji
Copy link
Contributor Author

zorji commented Jun 9, 2022

@adam-thomas-privitar Do you mind create a PR for this? I think it can be verified by a test.

@JasonShin
Copy link

JasonShin commented Jul 7, 2022

hey @adam-thomas-privitar and @zorji. Sorry first time here

I was playing around with the suggested update but it seems like there's a circular constraint issue.

Updated change:

export type Exact<ParameterType, InputType extends ParameterType> = ParameterType extends Primitive
	? ParameterType
	: ParameterType extends [infer U]
		? InputType extends [infer B]
			? Array<Exact<U[0], B[0]>>
			: never
		: { [Key in keyof ParameterType]: Exact<ParameterType[Key], InputType[Key]> } & Record<Exclude<keyof InputType, KeysOfUnion<ParameterType>>, never>;

The only significant change that I can see is I'm matching on [infer T] instead Array<any> and Array<infer B>. When I try to match based on Array TS would throw a circular constraint error. I'm not sure why TS dislikes Array pattern...

Testing with [infer T] :
Screen Shot 2022-07-07 at 4 50 33 pm

also testing by passing in a variable:
Screen Shot 2022-07-07 at 4 59 15 pm

Testing with matching on Array<infer T>

Screen Shot 2022-07-07 at 4 52 18 pm

Also checking the exactness of array types wouldn't work in the above scenario

@zorji
Copy link
Contributor Author

zorji commented Jul 7, 2022

Hi @JasonShin @adam-thomas-privitar

Do you have a case where the type does not work?

I am trying it with array and it looks like it's working for me.

Screen Shot 2022-07-07 at 7 30 52 pm

import { Exact } from 'type-fest'

type Type = Array<{ code: string; name: string }>
const fn = <T extends Exact<Type, T>>(args: T) => args

// correct input
fn([{
  code: 'test',
  name: '1'
}])

// missing field
fn([{
  code: 'test'
}])

// incorrect type
fn([{
  code: 'test',
  name: 1
}])

// excess field
fn([{
  code: 'test',
  name: '1',
  excessField: '',
}])

Also, I would suggest open a new issue and link to this PR and provide the minimal reproduce code.

@JasonShin
Copy link

JasonShin commented Jul 7, 2022

@zorji Thanks, I was playing with the type more

I can only reproduce in the following weird intersection type

type Type = Array<{ test: string }> & Array<{ z: number }>
// Can't construct Exact type with errors saying it's trying to also match on array method 'pop' and etc etc
const fn = <T extends Exact<Type, T>>(args: T) => args;

// correct input
fn([{
  code: 'test',
  name: '1'
}])

// missing field
fn([{
  code: 'test'
}])

// incorrect type
fn([{
  code: 'test',
  name: 1
}])

// excess field
fn([{
  code: 'test',
  name: '1',
  excessField: '',
}])

But as per https://www.reddit.com/r/typescript/comments/m5wvrx/how_to_extract_proper_intersection_type_from_an/ and microsoft/TypeScript#43267 this is a very weird type.

Workaround for those who have control over their typing would be writing the intersection type

type Type = ({ code: string } & { name: string })[]
const fn = <T extends Exact<Type, T>>(args: T) => args;

// correct input
fn([{
  code: 'test',
  name: '1'
}])

// missing field
fn([{
  code: 'test'
}])

// incorrect type
fn([{
  code: 'test',
  name: 1
}])

// excess field
fn([{
  code: 'test',
  name: '1',
  excessField: '',
}])

The only problem would be if the user doesn't have control over the underlying module's type declaration but they are providing the weird intersection type that goes Array<{ ... }> & Array<{...}>, then they will get the error when constructing Exact type.

I don't think @adam-thomas-privitar's suggestion fixes this issue as well mmm

I will still raise an issue to see if anyone thinks this is important

Issue link: #419

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

5 participants