Exact Types
See original GitHub issueThis 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 https://github.com/Microsoft/TypeScript/issues/12936#issuecomment-267272371, which encompasses using a simpler syntax with a generic type to enable usage in expressions.
Issue Analytics
- State:
- Created 7 years ago
- Reactions:956
- Comments:238 (58 by maintainers)
Top GitHub Comments
If the
{| ... |}
syntax was adopted, we could build on mapped types so that you could writeand then you could write
Exact<User>
.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 amagnitude
function should have the type(p: exact Point2D) => number
to prevent passing aPoint3D
. 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 toT
, but this fails ifT
is an exact type. This is problematic because you might have some generic function that uses thisT & U -> T
principle, but invoke the function withT
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 toT | U
, but it’s not obvious how to apply this rule ifU
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 forn
and finds it won’t be happy to sees
, but “No” also seems wrong because we’ve violated the basicT -> 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.