-
Notifications
You must be signed in to change notification settings - Fork 12.7k
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
Comments
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 I think this might not I expect if exact type is introduced. |
Not sure if realted but FYI #7481 |
If the type Exact<T> = {|
[P in keyof T]: P[T]
|} and then you could write |
This is probably the last thing I miss from Flow, compared to TypeScript. The |
@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 😄 |
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: |
@rotemdan See the above answer, there's the generic type |
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 function test(a: only string, b: only User) {}; That was the best syntax I could find back then. Edit: 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) |
I was wondering, maybe both keywords could be introduced to describe two slightly different types of 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 (Edit: just a note about |
@ethanresnick Why do you believe that? |
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 😉) |
I don't like the pipe syntax inspired by Flow. Something like exact interface Foo {} |
@mohsen1 I'm sure most people would use the |
With I think interface Foo {}
type Bar = exact Foo |
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 🌹 |
@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 The usage of an |
We talked about this for quite a while. I'll try to summarize the discussion. Excess Property CheckingExact 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 typesRelated 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 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. Violations of Assumptions / Instantiation ProblemsWe have some basic tenets that exact types would invalidate. For example, it's assumed that a type It's also assumed that MiscellanyWhat is the meaning of 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 Summary: Use Cases NeededOur 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. |
This would help to perform mutations on generics. Currently if function reset<T extends Record<K, number>, K extends keyof T>(v: T, k: K) {
v[k] = 0 // Error: requires number to be assignable to T[K], but T[K] could be arbitrarily precise
}
function increment<T extends Record<K, number>, K extends keyof T>(v: T, k: K) {
v[k] = v[k] + 1 // Error: requires both T[K] to be assignable to number (ok) and number to be assignable to T[K]
} Of course we could write these functions without generics for interface BaseEntity {
id: string
}
function createOrm<Entity extends BaseEntity>() {
return {
reset<T extends Entity & Record<K, number>, K extends keyof T>(entity: T, key: K) {
this.update(entity, key, 0) // Error not fixable for now
},
increment<T extends Entity & Record<K, number>, K extends keyof T>(entity: T, key: K) {
this.update(entity, key, entity[key] + 1) // Error not fixable for now
},
update<T extends Entity, K extends keyof T>(entity: T, key: K, value: T[K]) {
entity[key] = value
// saves the entity here through DB request so uses entity.id
}
}
} In this case we need the generics to ensure the user code will pass an If we'd have a way to specify that |
This isn't the topic of the ticket, which is prohibiting additional properties on the value that are not present on the type. Your issue is related to lack of invariant constraints on writable properties #18770 and somewhat also the lack of lower bound constraints #14520 (edit: edited the linked issues) |
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
@Exifers while exact types may make it easier to implement the restrictions you want in that example, I don't think it's strictly necessary. You can instead restrict the K parameter, though you need to make some (safe) assertions inside the implementation. function createOrm<Entity extends BaseEntity>() {
return {
reset<T extends Entity, K extends { [P in keyof T]: 0 extends T[P] ? P : never }[keyof T]>(entity: T, key: K) {
this.update(entity, key, 0 as T[K]);
},
increment<T extends Entity, K extends { [P in keyof T]: number extends T[P] ? P : never }[keyof T]>(entity: T, key: K) {
this.update(entity, key, ((entity[key] as number) + 1) as T[K]);
},
update<T extends Entity, K extends keyof T>(entity: T, key: K, value: T[K]) {
entity[key] = value
}
}
} |
this feature is my white whale 🐳 there are definitely concrete use cases for exact types. folks have already mentioned the issue of i think a more compelling use case is dynamic database insertions in ORMs and query builders such as kysely. (related issues: 1, 2) // assume the "person" table only has a "name" column.
await db.insertInto('person').values({ name: "Foo", oops: "bad" ).execute();
// tsc's excess property checks will correctly flag this error IF you use an object literal.
const invalidPerson = {
name: "Bar",
oops: "worse",
};
await db.insertInto('person').values(invalidPerson).execute();
// this will pass type checking but blow up at runtime :( it's great that excess property checking can prevent some instances of this problem, but in practice, it's a very limited subset of instances. in this example, excess property checking and structural typing are working as intended. i believe a new, opt-in language feature would be ideal for this problem (and many others mentioned in this long-running thread). |
There are many use-cases that have presented over and over again. Saying "there are no use-cases" anymore feels in bad taste at this point. However, there is still the question of how to implement this feature in a way that doesn't have massive breaking changes. Step 1To start, object type spreads should be added: type C = {...A, ...B, foo: string} This would be similar but distinct from When Step 2New syntax for defining exact object types. There are two way this could be done:
Using {| a: string |} & {| b: number |} === never
{| a: string | number |} & {| a: number | null |} === {| a: number |} Step 3Encourage moving to object type spreads instead of Step 4Start migrating object types to be exact in many more cases Step 5Consider making object types exact by default |
Speaking of step 1, check this (ts playground) out. Some time ago I wrote a type that properly merges objects and simulates spread syntax for my @nikelborm/joiner library |
@nikelborm I know that this is already possible, but you should not have to jump through the hoops you did just to merge object types. Adding object type spread syntax is a good idea for Typescript regardless. |
@nmn The spread syntax is a great idea! So great in fact that I actually added the same thing to ArkType 😅
ArkType also implements undeclared key handling that works just like you describe when combined with spreading and intersections. The implementation and/or unit tests might be useful as a point of reference if someone decides to add one or both of these features to TypeScript. |
i strongly disagree that "exact by default" is the way to go. typescript's huge ecosystem is proof that its structural typing strategy fits most use cases. exact types can be added to the language without huge ecosystem overhauls or codemods. ultimately, exact types should be another tool in typescript's toolbelt to support specific use cases. they have trade-offs and should not be the default behavior. but i think we've listed enough such cases that there's a strong argument for adding them to the language.
|
I think for React we'd want to strongly encourage exact by default due to how it can catch mistakes in serialization boundaries for React Server Components. Inexact objets might pass extra arguments which then end up erroring at runtime. E.g. having a standard set of lint rules that lints against inexact. I think the lesson learned from Flow was that it's not very useful unless it's the default because to catch mistakes like this you wouldn't know beforehand that you need to opt-in to it. I think it doesn't necessarily have to be "the default" as much as it has to look good - aesthetically. If you have to type |
It's a bit of a nightmare waiting to happen due to exact objects not being assignable as writable inexact object arguments, and inexact objects not being assignable as exact objects. It would create a divide where you'd want to work with one of the other, or you'd need compatibility/conversion functions and the like to make things work. That's not something I would look forward to. I've said before that readonly objects would make things easier - because both exact and inexact objects can be assigned to a readonly inexact argument, allowing libraries to provide an interface that works with both object types. Writable objects are also unsafe currenly, so this also kills an existing type unsafety as well, and likely it's compatible with much existing code due to that type unsafety with using mutable objects. I'm confident that readonly objects (and probably with it explicitly read/write objects with stricter type safe assignability rules) is the way to go. I wouldn't say that knowing how to implement this is the problem - but rather a question of someone or some people being willing to do the work on creating it. |
If I want my whole codebase to use exact types, adding an utility would add lot of frictions as devs can forget to use it in some places. I think it should a config that we can enable in the |
@RyanCavanaugh has expressed concern in the past that developers would misuse exactness by applying it to all their APIs, leading to breaking changes downstream. While I don't think the potential for misuse is a good reason not to implement a feature, it should be considered when determining how explicitly it is applied. TS is structural because in most contexts, it is safe, simple, and performant to allow and ignore extra properties on an object. Exactness can be thought of as an additional constraint on an object. It narrows the set of allowed values the same way adding a property like There is a genuine issue in the community where TS devs see a config or lint rule called Applying exactness should always be an explicit part of a type's definition to avoid this confusion and emphasize it as a context-dependent decision like making a key optional. |
I used the word "consider" for a reason. But, side note, exact object types are just as much "structural typing" as the inexact object types that exist today. They just don't allow extra stuff. The codemods specifically was for migrating from
Typescript, intentionally, allows a bunch of unsafe behavior already when it comes to variance. These issues are similar and I could see the argument to allow converting between exact and inexact object types even if it is unsafe because most of Typescript is only really safe if you don't use mutation.
Exact object types are structural. Exact objects are safer and prevent extra data from slipping through and causing side effects. And they are just as simple, just a different set of rules. I see the argument about backwards compatibility and I understand the compromises needed to not break the ecosystem, but if we were to ignore all of that exact object types are the better fit in 99% of use-cases. Almost none of the code that uses Flow uses inexact object types even though it is still supported. Another side note. IMO, asking for implicitly allowing extra keys on an object type is akin to implicitly allowing extra values in a tuple type: const twoVals: [number, number] = [1, 2, 3, '4', true]; The value clearly satisfies the requirement of the first two elements being numbers. Why does it matter if it has extra values? Tuples already have the features we want for object types.
Would you make the argument that Tuples would be better if they were inexact by default and allowed extra keys and that it would fit most use-cases and exact tuple types are an extra feature that should be used explicitly? |
Converting safely can be done by extracting known keys to make an exact object, or cloning the object to make an inexact. But these are chores and runtime operations. I'm not sure how much benefit Exact types provide if you then use unsafe casts to use them, that seems like you're undoing the benefit that they provide. All this is why I say implement Besides, unsafe variance has a lot to do with legacy functionality that needs to be supported (DOM methods are usually mentioned), which doesn't apply here. It's a necessity not a preference.
Exact tuples is a side affect of tuples have a Be clear, I am not opposed to exact types. I'm opposed to creating a new divide like the CJS/ESM module divide that plagued us. That could have been handled better (synchronous require of non-top-level-await ESM modules from CJS, esModuleInterop issues, etc). Let's not do that again. Any effort at exact types needs to be well considered and planned. But still, this needs an implementer to go forward. |
I've got a mockup of exact types, implemented entirely in the existing type system. The real thing that is needed to make this work properly is a TL;DR: If there was a TypeError type which was understood by the compiler, it would be possible to build absolutely everything else in the existing type system. I know because I've done it already. |
Useful in it's own right - I presume there is a ticket for it somewhere. Doom in the type system - a lot of things are possible |
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.
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.
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.
The text was updated successfully, but these errors were encountered: