The Wayback Machine - https://web.archive.org/web/20210105203108/https://github.com/Microsoft/TypeScript/issues/12936
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

Exact Types #12936

Open
blakeembrey opened this issue Dec 15, 2016 · 177 comments
Open

Exact Types #12936

blakeembrey opened this issue Dec 15, 2016 · 177 comments

Comments

@blakeembrey
Copy link
Contributor

@blakeembrey blakeembrey commented Dec 15, 2016

This is a proposal to enable a syntax for exact types. A similar feature can be seen in Flow (https://flowtype.org/docs/objects.html#exact-object-types), but I would like to propose it as a feature used for type literals and not interfaces. The specific syntax I'd propose using is the pipe (which almost mirrors the Flow implementation, but it should surround the type statement), as it's familiar as the mathematical absolute syntax.

interface User {
  username: string
  email: string
}

const user1: User = { username: 'x', email: 'y', foo: 'z' } //=> Currently errors when `foo` is unknown.
const user2: Exact<User> = { username: 'x', email: 'y', foo: 'z' } //=> Still errors with `foo` unknown.

// Primary use-case is when you're creating a new type from expressions and you'd like the
// language to support you in ensuring no new properties are accidentally being added.
// Especially useful when the assigned together types may come from other parts of the application 
// and the result may be stored somewhere where extra fields are not useful.

const user3: User = Object.assign({ username: 'x' }, { email: 'y', foo: 'z' }) //=> Does not currently error.
const user4: Exact<User> = Object.assign({ username: 'x' }, { email: 'y', foo: 'z' }) //=> Will error as `foo` is unknown.

This syntax change would be a new feature and affect new definition files being written if used as a parameter or exposed type. This syntax could be combined with other more complex types.

type Foo = Exact<X> | Exact<Y>

type Bar = Exact<{ username: string }>

function insertIntoDb (user: Exact<User>) {}

Apologies in advance if this is a duplicate, I could not seem to find the right keywords to find any duplicates of this feature.

Edit: This post was updated to use the preferred syntax proposal mentioned at #12936 (comment), which encompasses using a simpler syntax with a generic type to enable usage in expressions.

@HerringtonDarkholme
Copy link
Contributor

@HerringtonDarkholme HerringtonDarkholme commented Dec 15, 2016

I would suggest the syntax is arguable here. Since TypeScript now allows leading pipe for union type.

class B {}

type A = | number | 
B

Compiles now and is equivalent to type A = number | B, thanks to automatic semicolon insertion.

I think this might not I expect if exact type is introduced.

@normalser
Copy link

@normalser normalser commented Dec 15, 2016

Not sure if realted but FYI #7481

@DanielRosenwasser
Copy link
Member

@DanielRosenwasser DanielRosenwasser commented Dec 15, 2016

If the {| ... |} syntax was adopted, we could build on mapped types so that you could write

type Exact<T> = {|
    [P in keyof T]: P[T]
|}

and then you could write Exact<User>.

@joshaber
Copy link
Member

@joshaber joshaber commented Dec 15, 2016

This is probably the last thing I miss from Flow, compared to TypeScript.

The Object.assign example is especially good. I understand why TypeScript behaves the way it does today, but most of the time I'd rather have the exact type.

@blakeembrey
Copy link
Contributor Author

@blakeembrey blakeembrey commented Dec 15, 2016

@HerringtonDarkholme Thanks. My initial issue has mentioned that, but I omitted it in the end as someone would have a better syntax anyway, turns out they do 😄

@DanielRosenwasser That looks a lot more reasonable, thanks!

@wallverb I don't think so, though I'd also like to see that feature exist 😄

@rotemdan
Copy link

@rotemdan rotemdan commented Dec 17, 2016

What if I want to express a union of types, where some of them are exact, and some of them are not? The suggested syntax would make it error-prone and difficult to read, even If extra attention is given for spacing:

|Type1| | |Type2| | Type3 | |Type4| | Type5 | |Type6|

Can you quickly tell which members of the union are not exact?

And without the careful spacing?

|Type1|||Type2||Type3||Type4||Type5||Type6|

(answer: Type3, Type5)

@blakeembrey
Copy link
Contributor Author

@blakeembrey blakeembrey commented Dec 17, 2016

@rotemdan See the above answer, there's the generic type Extact instead which is a more solid proposal than mine. I think this is the preferred approach.

@rotemdan
Copy link

@rotemdan rotemdan commented Dec 17, 2016

There's also the concern of how it would look in editor hints, preview popups and compiler messages. Type aliases currently just "flatten" to raw type expressions. The alias is not preserved so the incomperhensible expressions would still appear in the editor, unless some special measures are applied to counteract that.

I find it hard to believe this syntax was accepted into a programming language like Flow, which does have unions with the same syntax as Typescript. To me it doesn't seem wise to introduce a flawed syntax that is fundamentally in conflict with existing syntax and then try very hard to "cover" it.

One interesting (amusing?) alternative is to use a modifier like only. I had a draft for a proposal for this several months ago, I think, but I never submitted it:

function test(a: only string, b: only User) {};

That was the best syntax I could find back then.

Edit: just might also work?

function test(a: just string, b: just User) {};

(Edit: now that I recall that syntax was originally for a modifier for nominal types, but I guess it doesn't really matter.. The two concepts are close enough so these keywords might also work here)

@rotemdan
Copy link

@rotemdan rotemdan commented Dec 19, 2016

I was wondering, maybe both keywords could be introduced to describe two slightly different types of matching:

  • just T (meaning: "exactly T") for exact structural matching, as described here.
  • only T (meaning: "uniquely T") for nominal matching.

Nominal matching could be seen as an even "stricter" version of exact structural matching. It would mean that not only the type has to be structurally identical, the value itself must be associated with the exact same type identifier as specified. This may or may not support type aliases, in addition to interfaces and classes.

I personally don't believe the subtle difference would create that much confusion, though I feel it is up to the Typescript team to decide if the concept of a nominal modifier like only seems appropriate to them. I'm only suggesting this as an option.

(Edit: just a note about only when used with classes: there's an ambiguity here on whether it would allow for nominal subclasses when a base class is referenced - that needs to be discussed separately, I guess. To a lesser degree - the same could be considered for interfaces - though I don't currently feel it would be that useful)

@ethanresnick
Copy link
Contributor

@ethanresnick ethanresnick commented Jan 8, 2017

This seems sort of like subtraction types in disguise. These issues might be relevant: #4183 #7993

@blakeembrey
Copy link
Contributor Author

@blakeembrey blakeembrey commented Jan 8, 2017

@ethanresnick Why do you believe that?

@johnnyreilly
Copy link

@johnnyreilly johnnyreilly commented Jan 12, 2017

This would be exceedingly useful in the codebase I'm working on right now. If this was already part of the language then I wouldn't have spent today tracking down an error.

(Perhaps other errors but not this particular error 😉)

@mohsen1
Copy link
Contributor

@mohsen1 mohsen1 commented Feb 17, 2017

I don't like the pipe syntax inspired by Flow. Something like exact keyword behind interfaces would be easier to read.

exact interface Foo {}
@blakeembrey
Copy link
Contributor Author

@blakeembrey blakeembrey commented Feb 18, 2017

@mohsen1 I'm sure most people would use the Exact generic type in expression positions, so it shouldn't matter too much. However, I'd be concerned with a proposal like that as you might be prematurely overloading the left of the interface keyword which has previously been reserved for only exports (being consistent with JavaScript values - e.g. export const foo = {}). It also indicates that maybe that keyword is available for types too (e.g. exact type Foo = {} and now it'll be export exact interface Foo {}).

@mohsen1
Copy link
Contributor

@mohsen1 mohsen1 commented Feb 19, 2017

With {| |} syntax how would extends work? will interface Bar extends Foo {| |} be exact if Foo is not exact?

I think exact keyword makes it easy to tell if an interface is exact. It can (should?) work for type too.

interface Foo {}
type Bar = exact Foo
@basarat
Copy link
Contributor

@basarat basarat commented Feb 19, 2017

Exceedingly helpful for things that work over databases or network calls to databases or SDKs like AWS SDK which take objects with all optional properties as additional data gets silently ignored and can lead to hard to very hard to find bugs 🌹

@blakeembrey
Copy link
Contributor Author

@blakeembrey blakeembrey commented Feb 19, 2017

@mohsen1 That question seems irrelevant to the syntax, since the same question still exists using the keyword approach. Personally, I don't have a preferred answer and would have to play with existing expectations to answer it - but my initial reaction is that it shouldn't matter whether Foo is exact or not.

The usage of an exact keyword seems ambiguous - you're saying it can be used like exact interface Foo {} or type Foo = exact {}? What does exact Foo | Bar mean? Using the generic approach and working with existing patterns means there's no re-invention or learning required. It's just interface Foo {||} (this is the only new thing here), then type Foo = Exact<{}> and Exact<Foo> | Bar.

@RyanCavanaugh
Copy link
Member

@RyanCavanaugh RyanCavanaugh commented Mar 7, 2017

We talked about this for quite a while. I'll try to summarize the discussion.

Excess Property Checking

Exact types are just a way to detect extra properties. The demand for exact types dropped off a lot when we initially implemented excess property checking (EPC). EPC was probably the biggest breaking change we've taken but it has paid off; almost immediately we got bugs when EPC didn't detect an excess property.

For the most part where people want exact types, we'd prefer to fix that by making EPC smarter. A key area here is when the target type is a union type - we want to just take this as a bug fix (EPC should work here but it's just not implemented yet).

All-optional types

Related to EPC is the problem of all-optional types (which I call "weak" types). Most likely, all weak types would want to be exact. We should just implement weak type detection (#7485 / #3842); the only blocker here is intersection types which require some extra complexity in implementation.

Whose type is exact?

The first major problem we see with exact types is that it's really unclear which types should be marked exact.

At one end of the spectrum, you have functions which will literally throw an exception (or otherwise do bad things) if given an object with an own-key outside of some fixed domain. These are few and far between (I can't name an example from memory). In the middle, there are functions which silently ignore
unknown properties (almost all of them). And at the other end you have functions which generically operate over all properties (e.g. Object.keys).

Clearly the "will throw if given extra data" functions should be marked as accepting exact types. But what about the middle? People will likely disagree. Point2D / Point3D is a good example - you might reasonably say that a magnitude function should have the type (p: exact Point2D) => number to prevent passing a Point3D. But why can't I pass my { x: 3, y: 14, units: 'meters' } object to that function? This is where EPC comes in - you want to detect that "extra" units property in locations where it's definitely discarded, but not actually block calls that involve aliasing.

Violations of Assumptions / Instantiation Problems

We have some basic tenets that exact types would invalidate. For example, it's assumed that a type T & U is always assignable to T, but this fails if T is an exact type. This is problematic because you might have some generic function that uses this T & U -> T principle, but invoke the function with T instantiated with an exact type. So there's no way we could make this sound (it's really not OK to error on instantiation) - not necessarily a blocker, but it's confusing to have a generic function be more permissive than a manually-instantiated version of itself!

It's also assumed that T is always assignable to T | U, but it's not obvious how to apply this rule if U is an exact type. Is { s: "hello", n: 3 } assignable to { s: string } | Exact<{ n: number }>? "Yes" seems like the wrong answer because whoever looks for n and finds it won't be happy to see s, but "No" also seems wrong because we've violated the basic T -> T | U rule.

Miscellany

What is the meaning of function f<T extends Exact<{ n: number }>(p: T) ? 😕

Often exact types are desired where what you really want is an "auto-disjointed" union. In other words, you might have an API that can accept { type: "name", firstName: "bob", lastName: "bobson" } or { type: "age", years: 32 } but don't want to accept { type: "age", years: 32, firstName: 'bob" } because something unpredictable will happen. The "right" type is arguably { type: "name", firstName: string, lastName: string, age: undefined } | { type: "age", years: number, firstName: undefined, lastName: undefined } but good golly that is annoying to type out. We could potentially think about sugar for creating types like this.

Summary: Use Cases Needed

Our hopeful diagnosis is that this is, outside of the relatively few truly-closed APIs, an XY Problem solution. Wherever possible we should use EPC to detect "bad" properties. So if you have a problem and you think exact types are the right solution, please describe the original problem here so we can compose a catalog of patterns and see if there are other solutions which would be less invasive/confusing.

@papb
Copy link

@papb papb commented Aug 13, 2020

@heystewart Your Exact does not give a symmetric result:

let a: Exact< { foo: number }[], { foo: number, bar?: string }[] >;
let b: Exact< { foo: number, bar?: string }[], { foo: number }[] >;

a = [{ foo: 123, bar: 'bar' }]; // error
b = [{ foo: 123, bar: 'bar' }]; // no error

Edit: @ArnaudBarre's version also has the same issue

@ArnaudBarre
Copy link

@ArnaudBarre ArnaudBarre commented Aug 13, 2020

@papb Yes effectively my typing doesn't work is the entry point is an array. I needed it for our graphQL API where variables is always an object.

To solve it you need to isolated ExactObject and ExactArray and have an entry point that goes into one or the other.

@captain-yossarian
Copy link

@captain-yossarian captain-yossarian commented Sep 16, 2020

So what the best way to make sure that object has exact properties, no less, no more ?

@toriningen
Copy link

@toriningen toriningen commented Sep 16, 2020

@captain-yossarian convince TypeScript team to implement this. No solution presented here works for all expected cases, and almost all of them lack clarity.

@captain-yossarian
Copy link

@captain-yossarian captain-yossarian commented Sep 16, 2020

@toriningen can't imagine how many issues will be closed if TS team will implement this feature

@rasenplanscher
Copy link

@rasenplanscher rasenplanscher commented Oct 18, 2020

@RyanCavanaugh
At present, I have one use-case that brought me here, and it runs right into your topic “Miscellany”. I want a function that:

  1. takes a parameter that implements an interface with optional parameters
  2. returns an object typed to the narrower actual interface of the given parameter, so that

Those immediate goals serve these ends:

  1. I get excess property checking for the input
  2. I get auto completion and property type-safety for the output

Example

I have reduced my case to this:

type X = {
    red?: number,
    green?: number,
    blue?: number,
}

function y<
    Y extends X
>(
    y: (X extends Y ? Y : X)
) {
    if ((y as any).purple) throw Error('bla')

    return y as Y
}

const z = y({
    blue: 1,
    red: 3,
    purple: 4, // error
})
z.green // error

type Z = typeof z

That setup works and accomplishes all the desired goals, so from a pure feasibility standpoint and so far as this goes, I'm good. However, EPC is achieved through the parameter typing (X extends Y ? Y : X). I basically stumbled on that by chance, and I was somewhat surprised that it worked.

Proposal

And that is why I'd like to have an implements keyword that can be used in place of extends in order to mark the intention that the type here is not supposed to have excess properties. Like so:

type X = {
    red?: number,
    green?: number,
    blue?: number,
}

function x<
    Y implements X
>( y: Y ) {
    if ((y as any).purple) throw Error('bla')

    return y as Y
}

const z = y({
    blue: 1,
    red: 3,
    purple: 4, // error
})
z.green // error

type Z = typeof z

This seems far clearer to me than my current workaround. Apart from being more concise, it locates the whole constraint with the generics declaration as opposed to my current split between the generics and the parameters.

That may also enable further use-cases that are currently impossible or impractical, but that is presently only a gut feeling.

Weak Type Detection as an Alternative

Notably, Weak Type Detection as per #3842 should fix that just as well, and might be favorable on account of not requiring additional syntax, if it worked in connection with extends, as per my use-case.

Regarding Exact<Type> etc.

Finally, implements, as I envision it, should be pretty straight-forward regarding your point about function f<T extends Exact<{ n: number }>(p: T) since it does not try to solve the more general case of Exact<Type>.

Generally, Exact<Type> seems to be of rather little utility next to EPC, and I cannot envision a valid generally useful case that falls outside these groups:

  • function calls: these can be easily handled now as per my example, and would benefit from implements
  • assignments: just use literals, so EPC applies
  • data from outside your domain of control: type-checking cannot guard you against that, you have to handle that at runtime, at which point you're back to safe casts

Obviously, there will be cases when you cannot assign literals, but these should also be of a finite set:

  • if you are given the assignment data in a function, handle the type check in the call signature
  • if you merge several objects, as per OP, then assert each source object's type properly and you can safely cast as DesiredType

Summary: implements would be nice but otherwise we're good

In summary, I'm confident that with implements and fixing EPC (if and when issues arise), exact types should really be handled.

Question to all interested parties: is anything actually open here?

Having looked through the use-cases here, I think that almost all repros are properly handled by now, and the rest can be made to work with my little example above. That begs the question: does anybody still have issues concerning this today with up-to-date TS?

@xp44mm
Copy link

@xp44mm xp44mm commented Nov 13, 2020

I have an immature idea About type annotations. Matching an object is divided into members can be exactly equal, no more and no less, more or less, no more but less, more but no less. For each of the above cases, there should be one expression.

exactly equal, i.e. no more and no less:

function foo(p:{|x:any,y:any|})

//it matched 
foo({x,y})
//no match
foo({x})
foo({y})
foo({x,y,z})
foo({})

more but no less:

function foo(p:{|x:any,y:any, ...|})

//it matched 
foo({x,y})
foo({x,y,z})

//no matched
foo({x})
foo({y})
foo({x,z})

no more but less:

function foo(p:{x:any,y:any})

//it matched 
foo({x,y})
foo({x})
foo({y})

//no match
foo({x,z})
foo({x,y,z})

more or less:

function foo(p:{x:any,y:any, ...})

//it matched 
foo({x,y})
foo({x})
foo({y})
foo({x,z})
foo({x,y,z})

conclusion:

With a vertical line indicates that there is no less, without a vertical line means that there can be less. With an ellipsis sign means that there can be more, without an ellipsis sign means that there can be no more. Arrays match is the same idea.

function foo(p:[|x,y|]) // p.length === 2
function foo(p:[|x,y, ... |]) // p.length >= 2
function foo(p:[x,y]) // p.length >= 0
function foo(p:[x,y,...]) // p.length >= 0
@stephenh
Copy link

@stephenh stephenh commented Nov 13, 2020

@rasenplanscher using your example, this compiles:

const x = { blue: 1, red: 3, purple: 4 };
const z = y(x);

However with exact types, it should not. I.e. the ask here is to not depend on EPC.

@noppa
Copy link

@noppa noppa commented Nov 13, 2020

@xp44mm "more but no less" is already the behaviour and "more or less" is the behaviour if you mark all properties optional

function foo(p:{x?: any, y?: any}) {}
const x = 1, y = 1, z = 1
// all pass
foo({x,y})
foo({x})
foo({y})
const p1 = {x,z}
foo(p1)
const p2 = {x,y,z}
foo(p2)

Similarily, if we had exact types, exact type + all properties optional would essentially be "no more but less" .

@infacto
Copy link

@infacto infacto commented Dec 2, 2020

Another example to this issue. A good demonstration for this proposal I think. In this case I use rxjs to work with Subject but want to return a ("locked") Observable (which has no next, error, etc. method to manipulate the value.)

someMethod(): Observable<MyType> {
  const subject = new Subject<MyType>();
  
  // This works, but should not. (if this proposal is implemented.)
  return subject;

  // Only Observable should be allowed as return type.
  return subject.asObservable();
}

I always want only return the exact type Observable and not Subject which extends it.

Proposal:

// Adding exclamation mark `!` (or something else) to match exact type. (or some other position `method(): !Foo`, ...)
someMethod()!: Observable<MyType> {
  // ...
}

But I'm sure you have better ideas. Especially because this does not only affects return values, right? Anyway, just a pseudo code demo. I think that would a nice feature to avoid errors and lacks. Like in the case described above. Another solution could be adding a new Utility Type.
Or did I miss something? Does this already work? I use TypeScript 4.

@ayroblu
Copy link

@ayroblu ayroblu commented Dec 11, 2020

Hi, I'm not sure where this is at, but this seems like a key feature that's missing. I made a playground but I think everyone here knows the issue.

I think the best solution is adding a flag, similar to the noUncheckedIndexedAccess flag that was added in 4.1, or perhaps under the strict section, I'm not sure what the criteria is, but I generally enable them all. I find it unlikely generally that you want the inexact type in a strict TS setting, though having an Inexact type is possible here for the few cases where that's reasonable.

@migueloller
Copy link

@migueloller migueloller commented Dec 16, 2020

Perhaps TypeScript doesn't have support for exact object types, but one could still validate that some type matches some other type exactly:

type Exact<T, U> = T extends U
  ? Exclude<keyof T, keyof U> extends never
    ? T
    : never
  : never

This can be used like this:

interface Point {
  x: number
  y: number
}

function addPoints<A, B>(a: Exact<A, Point>, b: Exact<B, Point>): Point {
  // add the points as vectors... 
}

https://www.typescriptlang.org/play?#code/C4TwDgpgBAogHgQwMbADwBUA0UCqA+KAXinSgjmAgDsATAZ1wCgooB+WOJAGwFcaJUAawggA9gDMS2YWMn4yFavShUIANwgAnZizYkdLAFwr1WncdUbtOgJZVKm8cmgAFUXeBQA3jrgWeALYARmYsIP7BZgC+jIziPFQoNqJUUAg0NG4edKgAgtgAQngAFAjG8Mho+VBZ9njYQeWIKKgF2LXAeACUxmruNN46APRDaRlQwAAW0GDu9gwIDBooopp0AHSbUIwxjOmZc8B0xV5QflAADNjhl1BR2KfnV1A3z+KiosYA5EEIml93Lp7DIdY6PYzPV53B5nCHXCGAoA

@ArnaudBarre
Copy link

@ArnaudBarre ArnaudBarre commented Dec 16, 2020

@migueloller It would not be a 4 years old issue if it was that simple ;) playground

@migueloller
Copy link

@migueloller migueloller commented Dec 16, 2020

@ArnaudBarre, we can leverage recursive types to fix this: Playground

type Exact<T, U> = T extends Record<string, unknown>
  ? T extends U
    ? Exclude<keyof T, keyof U> extends never
      ? { [K in keyof U]: Exact<T[K], U[K]> }
      : never
    : never
  : T
@lazytype
Copy link

@lazytype lazytype commented Dec 17, 2020

It's still not that easy. Playground

@migueloller
Copy link

@migueloller migueloller commented Dec 17, 2020

@lazytype, @ArnaudBarre, I'm seeing now the older comments (GitHub was hiding them because the discussion is so long). Sorry for re-stating what was already obvious/had already been explored.

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

Successfully merging a pull request may close this issue.

You can’t perform that action at this time.