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 https://github.com/Microsoft/TypeScript/issues/12936#issuecomment-267272371, which encompasses using a simpler syntax with a generic type to enable usage in expressions.
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.
Not sure if realted but FYI https://github.com/Microsoft/TypeScript/issues/7481
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>
.
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.
@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: Type3
, Type5
)
@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.
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)_
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)_
This seems sort of like subtraction types in disguise. These issues might be relevant: https://github.com/Microsoft/TypeScript/issues/4183 https://github.com/Microsoft/TypeScript/issues/7993
@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
keyword behind interfaces would be easier to read.
exact interface Foo {}
@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 {}
).
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
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 :rose:
@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
.
We talked about this for quite a while. I'll try to summarize the discussion.
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).
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.
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.
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.
What is the meaning of function f<T extends Exact<{ n: number }>(p: T)
? :confused:
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.
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.
The main place I see people get surprised by having no exact object type is in the behaviour of Object.keys
and for..in
-- they always produce a string
type instead of 'a'|'b'
for something typed { a: any, b: any }
.
As I mentioned in https://github.com/Microsoft/TypeScript/issues/14094 and you described in Miscellany section it's annoying that {first: string, last: string, fullName: string}
conforms to {first: string; last: string} | {fullName: string}
.
For example, it's assumed that a type T & U is always assignable to T, but this fails if T is an exact type
If T
is an exact type, then presumably T & U
is never
(or T === U
). Right?
Or U
is a non-exact subset of T
My use case that lead me to this suggestion are redux reducers.
interface State {
name: string;
}
function nameReducer(state: State, action: Action<string>): State {
return {
...state,
fullName: action.payload // compiles, but it's an programming mistake
}
}
As you pointed out in the summary, my issue isn't directly that I need exact interfaces, I need the spread operator to work precisely. But since the behavior of the spread operator is given by JS, the only solution that comes to my mind is to define the return type or the interface to be exact.
Do I understand correctly that assigning a value of T
to Exact<T>
would be an error?
interface Dog {
name: string;
isGoodBoy: boolean;
}
let a: Dog = { name: 'Waldo', isGoodBoy: true };
let b: Exact<Dog> = a;
In this example, narrowing Dog
to Exact<Dog>
would not be safe, right?
Consider this example:
interface PossiblyFlyingDog extends Dog {
canFly: boolean;
}
let c: PossiblyFlyingDog = { ...a, canFly: true };
let d: Dog = c; // this is okay
let e: Exact<Dog> = d; // but this is not
@leonadler Yes, that'd be the idea. You could only assign Exact<T>
to Exact<T>
. My immediate use-case is that validation functions would be handling the Exact
types (e.g. taking request payloads as any
and outputting valid Exact<T>
). Exact<T>
, however, would be assignable to T
.
@nerumo
As you pointed out in the summary, my issue isn't directly that I need exact interfaces, I need the spread operator to work precisely. But since the behavior of the spread operator is given by JS, the only solution that comes to my mind is to define the return type or the interface to be exact.
I have bumped on the same issue and figured out this solution which for me is quite elegant workaround :)
export type State = {
readonly counter: number,
readonly baseCurrency: string,
};
// BAD
export function badReducer(state: State = initialState, action: Action): State {
if (action.type === INCREASE_COUNTER) {
return {
...state,
counterTypoError: state.counter + 1, // OK
}; // it's a bug! but the compiler will not find it
}
}
// GOOD
export function goodReducer(state: State = initialState, action: Action): State {
let partialState: Partial<State> | undefined;
if (action.type === INCREASE_COUNTER) {
partialState = {
counterTypoError: state.counter + 1, // Error: Object literal may only specify known properties, and 'counterTypoError' does not exist in type 'Partial<State>'.
}; // now it's showing a typo error correctly
}
if (action.type === CHANGE_BASE_CURRENCY) {
partialState = { // Error: Types of property 'baseCurrency' are incompatible. Type 'number' is not assignable to type 'string'.
baseCurrency: 5,
}; // type errors also works fine
}
return partialState != null ? { ...state, ...partialState } : state;
}
you can find more in this section of my redux guide:
Note that this could be solved in userland using my constraint types proposal (#13257):
type Exact<T> = [
case U in U extends T && T extends U: T,
];
Edit: Updated syntax relative to proposal
@piotrwitek thank you, the Partial trick works perfectly and already found a bug in my code base ;) that's worth the little boilerplate code. But still I agree with @isiahmeadows that an Exact
@piotrwitek using Partial like that _almost_ solved my problem, but it still allows the properties to become undefined even if the State interface clams they aren't (I'm assuming strictNullChecks).
I ended up with something slightly more complex to preserve the interface types:
export function updateWithPartial<S extends object>(current: S, update: Partial<S>): S {
return Object.assign({}, current, update);
}
export function updateWith<S extends object, K extends keyof S>(current: S, update: {[key in K]: S[key]}): S {
return Object.assign({}, current, update);
}
interface I {
foo: string;
bar: string;
}
const f: I = {foo: "a", bar: "b"}
updateWithPartial(f, {"foo": undefined}).foo.replace("a", "x"); // Compiles, but fails at runtime
updateWith(f, {foo: undefined}).foo.replace("a", "x"); // Does not compile
updateWith(f, {foo: "c"}).foo.replace("a", "x"); // Compiles and works
@asmundg that is correct, the solution will accept undefined, but from my point of view this is acceptable, because in my solutions I'm using only action creators with required params for payload, and this will ensure that no undefined value should ever be assigned to a non-nullable property.
Practically I'm using this solution for quite some time in production and this problem never happened, but let me know your concerns.
export const CHANGE_BASE_CURRENCY = 'CHANGE_BASE_CURRENCY';
export const actionCreators = {
changeBaseCurrency: (payload: string) => ({
type: CHANGE_BASE_CURRENCY as typeof CHANGE_BASE_CURRENCY, payload,
}),
}
store.dispatch(actionCreators.changeBaseCurrency()); // Error: Supplied parameters do not match any signature of call target.
store.dispatch(actionCreators.changeBaseCurrency(undefined)); // Argument of type 'undefined' is not assignable to parameter of type 'string'.
store.dispatch(actionCreators.changeBaseCurrency('USD')); // OK => { type: "CHANGE_BASE_CURRENCY", payload: 'USD' }
DEMO - enable strictNullChecks in options
you can also make a nullable payload as well if you need to, you can read more in my guide: https://github.com/piotrwitek/react-redux-typescript-guide#actions
When Rest Types get merged in, this feature can be easily made syntactic sugar over them.
The type equality logic should be made strict - only types with the same properties or types which have rest properties that can be instantiated in such a way that their parent types have the same properties are considered matching. To preserve backward compatibility, a synthetic rest type is added to all types unless one already exists. A new flag --strictTypes
is also added, which suppresses the addition of synthetic rest parameters.
Equalities under --strictTypes
:
type A = { x: number, y: string };
type B = { x: number, y: string, ...restB: <T>T };
type C = { x: number, y: string, z: boolean, ...restC: <T>T };
declare const a: A;
declare const b: B;
declare const c: C;
a = b; // Error, type B has extra property: "restB"
a = c; // Error, type C has extra properties: "z", "restC"
b = a; // OK, restB inferred as {}
b = c; // OK, restB inferred as { z: boolean, ...restC: <T>T }
c = a; // Error, type A is missing property: "z"
// restC inferred as {}
c = b; // Error, type B is missing property: "z"
// restC inferred as restB
If --strictTypes
is not switched on a ...rest: <T>T
property is automatically added on type A
. This way the lines a = b;
and a = c;
will no longer be errors, as is the case with variable b
on the two lines that follow.
it's assumed that a type T & U is always assignable to T, but this fails if T is an exact type.
Yes, &
allows bogus logic but so is the case with string & number
. Both string
and number
are distinct rigid types that cannot be intersected, however the type system allows it. Exact types are also rigid, so the inconsistency is still consistent. The problem lies in the &
operator - it's unsound.
Is { s: "hello", n: 3 } assignable to { s: string } | Exact<{ n: number }>.
This can be translated to:
type Test = { s: string, ...rest: <T>T } | { n: number }
const x: Test = { s: "hello", n: 3 }; // OK, s: string; rest inferred as { n: number }
So the answer should be "yes". It's unsafe to union exact with non-exact types, as the non-exact types subsume all exact types unless a discriminator property is present.
Re: the function f<T extends Exact<{ n: number }>(p: T)
in @RyanCavanaugh's comment above, in one of my libraries I would very much like to implement the following function:
const checkType = <T>() => <U extends Exact<T>>(value: U) => value;
I.e. a function that returns it's parameter with its exact same type, but at the same time also check whether it's type is also exactly the same type as another (T).
Here is a bit contrived example with three of my failed tries to satisfy both requirements:
CorrectObject
HasX
without specifying HasX
as the object's typetype AllowedFields = "x" | "y";
type CorrectObject = {[field in AllowedFields]?: number | string};
type HasX = { x: number };
function objectLiteralAssignment() {
const o: CorrectObject = {
x: 1,
y: "y",
// z: "z" // z is correctly prevented to be defined for o by Excess Properties rules
};
const oAsHasX: HasX = o; // error: Types of property 'x' are incompatible.
}
function objectMultipleAssignment() {
const o = {
x: 1,
y: "y",
z: "z",
};
const o2 = o as CorrectObject; // succeeds, but undesirable property z is allowed
type HasX = { x: number };
const oAsHasX: HasX = o; // succeeds
}
function genericExtends() {
const checkType = <T>() => <U extends T>(value: U) => value;
const o = checkType<CorrectObject>()({
x: 1,
y: "y",
z: "z", // undesirable property z is allowed
}); // o is inferred to be { x: number; y: string; z: string; }
type HasX = { x: number };
const oAsHasX: HasX = o; // succeeds
}
Here HasX
is a greatly simplified type (the actual type maps o against a schema type) which is defined in a different layer than the constant itself, so I can't make o
's type to be (CorrectObject & HasX
).
With Exact Types, the solution would be:
function exactTypes() {
const checkType = <T>() => <U extends Exact<T>>(value: U) => value;
const o = checkType<CorrectObject>()({
x: 1,
y: "y",
// z: "z", // undesirable property z is *not* allowed
}); // o is inferred to be { x: number; y: string; }
type HasX = { x: number };
const oAsHasX: HasX = o; // succeeds
}
@andy-ms
If T is an exact type, then presumably T & U is never (or T === U). Right?
I think T & U
should be never
only if U
is provably incompatible with T
, e.g. if T
is Exact<{x: number | string}>
and U
is {[field: string]: number}
, then T & U
should be Exact<{x: number}>
See the first response to that:
Or U is a non-exact subset of T
I would say, if U is assignable to T, then T & U === T
. But if T
and U
are different exact types, then T & U === never
.
In your example, why is it necessary to have a checkType
function that does nothing? Why not just have const o: Exact<CorrectObject> = { ... }
?
Because it loses the information that x definitely exists (optional in CorrectObject) and is number (number | string in CorrectObject). Or perhaps I've misunderstood what Exact means, I thought it would just prevent extraneous properties, not that it would recurively mean all types must be exactly the same.
One more consideration in support for Exact Types and against the current EPC is refactoring - if Extract Variable refactoring was available, one would lose EPC unless the extracted variable introduced a type annotation, which could become very verbose.
To clarify why I supoort for Exact Types - it's not for discriminated unions but spelling errors and erronously extraneous properties in case the type costraint cannot be specified at the same time as the object literal.
@andy-ms
I would say, if U is assignable to T, then T & U === T. But if T and U are different exact types, then T & U === never.
The &
type operator is intersection operator, the result of it is the common subset of both sides, which doesn't necessarily equal either. Simplest example I can think of:
type T = Exact<{ x?: any, y: any }>;
type U = { x: any, y? any };
here T & U
should be Exact<{ x: any, y: any }>
, which is a subset of both T
and U
, but neither T
is a subset of U
(missing x) nor U
is a subset of T
(missing y).
This should work independent of whether T
, U
, or T & U
are exact types.
@magnushiie You have a good point -- exact types can limit assignability from types with a greater width, but still allow assignability from types with a greater depth. So you could intersect Exact<{ x: number | string }>
with Exact<{ x: string | boolean }>
to get Exact<{ x: string }>
. One problem is that this isn't actually typesafe if x
isn't readonly -- we might want to fix that mistake for exact types, since they mean opting in to stricter behavior.
Exact types could also be used for type arguments relations issues to index signatures.
interface T {
[index: string]: string;
}
interface S {
a: string;
b: string;
}
interface P extends S {
c: number;
}
declare function f(t: T);
declare function f2(): P;
const s: S = f2();
f(s); // Error because an interface can have more fields that is not conforming to an index signature
f({ a: '', b: '' }); // No error because literals is exact by default
Here's a hacky way to check for exact type:
// type we'll be asserting as exact:
interface TextOptions {
alignment: string;
color?: string;
padding?: number;
}
// when used as a return type:
function getDefaultOptions(): ExactReturn<typeof returnValue, TextOptions> {
const returnValue = { colour: 'blue', alignment: 'right', padding: 1 };
// ERROR: ^^ Property 'colour' is missing in type 'TextOptions'.
return returnValue
}
// when used as a type:
function example(a: TextOptions) {}
const someInput = {padding: 2, colour: '', alignment: 'right'}
example(someInput as Exact<typeof someInput, TextOptions>)
// ERROR: ^^ Property 'colour' is missing in type 'TextOptions'.
Unfortunately it's not currently possible to make the Exact assertion as a type-parameter, so it has to be made during call time (i.e. you need to remember about it).
Here are the helper utils required to make it work (thanks to @tycho01 for some of them):
type Exact<A, B extends Difference<A, B>> = AssertPassThrough<Difference<A, B>, A, B>
type ExactReturn<A, B extends Difference<A, B>> = B & Exact<A, B>
type AssertPassThrough<Actual, Passthrough, Expected extends Actual> = Passthrough;
type Difference<A, Without> = {
[P in DiffUnion<keyof A, keyof Without>]: A[P];
}
type DiffUnion<T extends string, U extends string> =
({[P in T]: P } &
{ [P in U]: never } &
{ [k: string]: never })[T];
See: Playground.
Nice one! @gcanti (typelevel-ts
) and @pelotom (type-zoo
) might be interested as well. :)
To anyone interested, I found a simple way of enforcing exact types on function parameters. Works on TS 2.7, at least.
function myFn<T extends {[K in keyof U]: any}, U extends DesiredType>(arg: T & U): void;
EDIT: I guess for this to work you must specify an object literal directly into the argument; this doesn't work if you declare a separate const above and pass that in instead. :/ But one workaround is to just use object spread at the call site, i.e., myFn({...arg})
.
EDIT: sorry, I didn't read that you mentioned TS 2.7 only. I will test it there!
@vaskevich I can't seem to get it to work, i.e. it's not detecting :colour
as an excess property
When conditional types land (#21316) you can do the following to require exact types as function parameters, even for "non-fresh" object literals:
type Exactify<T, X extends T> = T & {
[K in keyof X]: K extends keyof T ? X[K] : never
}
type Foo = {a?: string, b: number}
declare function requireExact<X extends Exactify<Foo, X>>(x: X): void;
const exact = {b: 1};
requireExact(exact); // okay
const inexact = {a: "hey", b: 3, c: 123};
requireExact(inexact); // error
// Types of property 'c' are incompatible.
// Type 'number' is not assignable to type 'never'.
Of course if you widen the type it won't work, but I don't think there's anything you can really do about that:
const inexact = {a: "hey", b: 3, c: 123} as Foo;
requireExact(inexact); // okay
Thoughts?
Looks like progress is being made on function parameters. Has anyone found a way to enforce exact types for a function return value?
@jezzgoodwin not really. See #241 which is the root cause of function returns not being properly checked for extra properties
One more use case. I've just almost run into a bug because of the following situation that is not reported as an error:
interface A {
field: string;
}
interface B {
field2: string;
field3?: string;
}
type AorB = A | B;
const fixture: AorB[] = [
{
field: 'sfasdf',
field3: 'asd' // ok?!
},
];
The obvious solution for this could be:
type AorB = Exact<A> | Exact<B>;
I saw a workaround proposed in #16679 but in my case, the type is AorBorC
(may grow) and each object have multiple properties, so I it's rather hard to manually compute set of fieldX?:never
properties for each type.
@michalstocki Isn't that #20863? You want excess property checking on unions to be stricter.
Anyway, in the absence of exact types and strict excess property checking on unions, you can do these fieldX?:never
properties programmatically instead of manually by using conditional types:
type AllKeys<U> = U extends any ? keyof U : never
type ExclusifyUnion<U> = [U] extends [infer V] ?
V extends any ?
(V & {[P in Exclude<AllKeys<U>, keyof V>]?: never})
: never : never
And then define your union as
type AorB = ExclusifyUnion<A | B>;
which expands out to
type AorB = (A & {
field2?: undefined;
field3?: undefined;
}) | (B & {
field?: undefined;
})
automatically. It works for any AorBorC
also.
Also see https://github.com/Microsoft/TypeScript/issues/14094#issuecomment-373780463 for exclusive or implementation
@jcalz The advanced type ExclusifyUnion isn't very safe:
const { ...fields } = o as AorB;
fields.field3.toUpperCase(); // it shouldn't be passed
The fields of fields
are all non-optional.
I don't think that has much to do with Exact types, but with what happens when you spread and then destructure a union-typed object . Any union will end up getting flattened out into a single intersection-like type, since it's pulling apart an object into individual properties and then rejoining them; any correlation or constraint between the constituents of each union will be lost. Not sure how to avoid it... if it's a bug, it might be a separate issue.
Obviously things will behave better if you do type guarding before the destructuring:
declare function isA(x: any): x is A;
declare function isB(x: any): x is B;
declare const o: AorB;
if (isA(o)) {
const { ...fields } = o;
fields.field3.toUpperCase(); // error
} else {
const { ...fields } = o;
fields.field3.toUpperCase(); // error
if (fields.field3) {
fields.field3.toUpperCase(); // okay
}
}
Not that this "fixes" the issue you see, but that's how I'd expect someone to act with a constrained union.
Maybe https://github.com/Microsoft/TypeScript/pull/24897 fixes the spread issue
i might be late to the party, but here is how you can at least make sure your types exactly match:
type AreSame<A, B> = A extends B
? B extends A ? true : false
: false;
const sureIsTrue: (fact: true) => void = () => {};
const sureIsFalse: (fact: false) => void = () => {};
declare const x: string;
declare const y: number;
declare const xAndYAreOfTheSameType: AreSame<typeof x, typeof y>;
sureIsFalse(xAndYAreOfTheSameType); // <-- no problem, as expected
sureIsTrue(xAndYAreOfTheSameType); // <-- problem, as expected
wish i could do this:
type Exact<A, B> = A extends B ? B extends A ? B : never : never;
declare function needExactA<X extends Exact<A, X>>(value: X): void;
Would the feature described in this issue help with a case where an empty/indexed interface matches object-like types, like functions or classes?
interface MyType
{
[propName: string]: any;
}
function test(value: MyType) {}
test({}); // OK
test(1); // Fails, OK!
test(''); // Fails, OK!
test(() => {}); // Does not fail, not OK!
test(console.log); // Does not fail, not OK!
test(console); // Does not fail, not OK!
Interface MyType
only defines an index signature and is used as the type of the only parameter of the function test
. Parameter passed to the function of type:
{}
, passes. Expected behavior.1
does not pass. Expected behavior (_Argument of type '1' is not assignable to parameter of type 'MyType'._)''
does not pass. Expected behavior (_`Argument of type '""' is not assignable to parameter of type 'MyType'._)() => {}
: Passes. Not expected behavior. Probably passes because functions are objects?console.log
Passes. Not expected behavior. Similar to arrow function.console
passes. Not expected behavior. Probably because classes are objects?The point is to only allow variables that exactly match the interface MyType
by being of that type already (and not implicitly converted to it). TypeScript seems to do a lot of implicit conversion based on signatures so this might be something that cannot be supported.
Apologies if this is off-topic. So far this issue is the closest match to the problem I explained above.
@Janne252 This proposal could help you indirectly. Assuming you tried the obvious Exact<{[key: string]: any}>
, here's why it would work:
{[key: string]: any}
.{[key: string]: any}
.{[key: string]: any}
.call
signature (it's not a string property).console
object passes because it's just that, an object (not a class). JS makes no separation between objects and key/value dictionaries, and TS is no different here apart from the added row-polymorphic typing. Also, TS doesn't support value-dependent types, and typeof
is simply sugar for adding a few extra parameters and/or type aliases - it's not nearly as magical as it looks.@blakeembrey @michalstocki @aleksey-bykov
This is my way of doing exact types:
type Exact<A extends object> = A & {__kind: keyof A};
type Foo = Exact<{foo: number}>;
type FooGoo = Exact<{foo: number, goo: number}>;
const takeFoo = (foo: Foo): Foo => foo;
const foo = {foo: 1} as Foo;
const fooGoo = {foo: 1, goo: 2} as FooGoo;
takeFoo(foo)
takeFoo(fooGoo) // error "[ts]
//Argument of type 'Exact<{ foo: number; goo: number; }>' is not assignable to parameter of type 'Exact<{ //foo: number; }>'.
// Type 'Exact<{ foo: number; goo: number; }>' is not assignable to type '{ __kind: "foo"; }'.
// Types of property '__kind' are incompatible.
// Type '"foo" | "goo"' is not assignable to type '"foo"'.
// Type '"goo"' is not assignable to type '"foo"'."
const takeFooGoo = (fooGoo: FooGoo): FooGoo => fooGoo;
takeFooGoo(fooGoo);
takeFooGoo(foo); // error "[ts]
// Argument of type 'Exact<{ foo: number; }>' is not assignable to parameter of type 'Exact<{ foo: number; // goo: number; }>'.
// Type 'Exact<{ foo: number; }>' is not assignable to type '{ foo: number; goo: number; }'.
// Property 'goo' is missing in type 'Exact<{ foo: number; }>'.
It works for functions parameters, returns and even for assingments.
const foo: Foo = fooGoo;
// error
No runtime overhead. Only issue is that whenever you create new exact object you have to cast it against its type, but it's not a big deal really.
I believe the original example has the correct behavior: I expect interface
s to be open. In contrast, I expect type
s to be closed (and they are only closed sometimes). Here is an example of surprising behavior when writing a MappedOmit
type:
https://gist.github.com/donabrams/b849927f5a0160081db913e3d52cc7b3
The MappedOmit
type in the example only works as intended for discriminated unions. For non discriminated unions, Typescript 3.2 is passing when any intersection of the types in the union is passed.
The workarounds above using as TypeX
or as any
to cast have the side effect of hiding errors in construction!. We want our typechecker to help us catch errors in construction too! Additionally, there are several things we can generate statically from well defined types. Workarounds like the above (or the nominal type workarounds described here: https://gist.github.com/donabrams/74075e89d10db446005abe7b1e7d9481) stop those generators from working (though we can filter _
leading fields, it's a painful convention that's absolutely avoidable).
@aleksey-bykov fyi i think your implementation is 99% of the way there, this worked for me:
type AreSame<A, B> = A extends B
? B extends A ? true : false
: false;
type Exact<A, B> = AreSame<A, B> extends true ? B : never;
const value1 = {};
const value2 = {a:1};
// works
const exactValue1: Exact<{}, typeof value1> = value1;
const exactValue1WithTypeof: Exact<typeof value1, typeof value1> = value1;
// cannot assign {a:number} to never
const exactValue1Fail: Exact<{}, typeof value2> = value2;
const exactValue1FailWithTypeof: Exact<typeof value1, typeof value2> = value2;
// cannot assign {} to never
const exactValue2Fail: Exact<{a: number}, typeof value1> = value1;
const exactValue2FailWithTypeof: Exact<typeof value2, typeof value1> = value1;
// works
const exactValue2: Exact<{a: number}, typeof value2> = value2;
const exactValue2WithTypeof: Exact<typeof value2, typeof value2> = value2;
wow, please leave the flowers over here, presents go in that bin
One small improvement that can be made here:
By using the following definition of Exact
effectively creates a subtraction of B
from A
as A
& never
types on all of B
's unique keys, you can get more granular errors on the invalid properties:
type Omit<T, K> = Pick<T, Exclude<keyof T, keyof K>>;
type Exact<A, B = {}> = A & Record<keyof Omit<B, A>, never>;
Lastly, I wanted to be able to do this without having to add explicit template usage of the second B
template argument. I was able to make this work by wrapping with a method- not ideal since it affects runtime but it is useful if you really really need it:
function makeExactVerifyFn<T>() {
return <C>(x: C & Exact<T, C>): C => x;
}
interface Task {
title: string;
due?: Date;
}
const isOnlyTask = makeExactVerifyFn<Task>();
const validTask_1 = isOnlyTask({
title: 'Get milk',
due: new Date()
});
const validTask_2 = isOnlyTask({
title: 'Get milk'
});
const invalidTask_1 = isOnlyTask({
title: 5 // [ts] Type 'number' is not assignable to type 'string'.
});
const invalidTask_2 = isOnlyTask({
title: 'Get milk',
procrastinate: true // [ts] Type 'true' is not assignable to type 'never'.
});
@danielnmsft It seems weird to leave B
in Exact<A, B>
optional in your example, especially if it's required for proper validation. Otherwise, it looks pretty good to me. It looks better named Equal
, though.
@drabinowitz Your type Exact
does not actually represent what has been proposed here and probably should be renamed to something like AreExact
. I mean, you can't do this with your type:
function takesExactFoo<T extends Exact<Foo>>(foo: T) {}
However, your type is handy to implement the exact parameter type!
type AreSame<A, B> = A extends B
? B extends A ? true : false
: false;
type Exact<A, B> = AreSame<A, B> extends true ? B : never;
interface Foo {
bar: any
}
function takesExactFoo <T>(foo: T & Exact<Foo, T>) {
// ^ or `T extends Foo` to type-check `foo` inside the function
}
let foo = {bar: 123}
let foo2 = {bar: 123, baz: 123}
takesExactFoo(foo) // ok
takesExactFoo(foo2) // error
UPD1 This will not create +1 runtime function as in the solution of @danielnmsft and of course is much more flexible.
UPD2 I just realized that Daniel in fact made basically the same type Exact
as @drabinowitz did, but a more compact and probably better one. I also realized that I did the same thing as Daniel had done. But I'll leave my comment in case if someone finds it useful.
That definition of AreSame
/Exact
does not seem to work for union type.
Example: Exact<'a' | 'b', 'a' | 'b'>
results in never
.
This can apparently be fixed by defining type AreSame<A, B> = A|B extends A&B ? true : false;
@nerumo definitely found this for the same type of reducer function you showed.
Couple additional options from what you had:
typeof
. More useful if it's a very complicated type. To me when I look at this it's more explicitly obvious the intent is prevent extra properties.interface State {
name: string;
}
function nameReducer(state: State, action: Action<string>): typeof state {
return {
...state,
fullName: action.payload // THIS IS REPORTED AS AN ERROR
};
}
interface State {
name: string;
}
function nameReducer(state: State, action: Action<string>) {
return (state = {
...state,
fullName: action.payload // THIS IS REPORTED AS AN ERROR
});
}
typeof state
againinterface State {
name: string;
}
function nameReducer(state: State, action: Action<string>) {
const newState: typeof state = {
...state,
fullName: action.payload // THIS IS REPORTED AS AN ERROR
};
return newState;
}
...state
you can use Partial<typeof state>
for the type:interface State {
name: string;
}
function nameReducer(state: State, action: Action<string>) {
const newState: Partial<typeof state> = {
name: 'Simon',
fullName: action.payload // THIS IS REPORTED AS AN ERROR
};
return newState;
}
I do feel this whole conversation (and I just read the whole thread) missed the crux of the issue for most people and that is that to prevent errors all we want is a type assertion to prevent disallow a 'wider' type:
This is what people may try first, which doesn't disallow 'fullName':
return <State> {
...state,
fullName: action.payload // compiles ok :-(
};
This is because <Dog> cat
is you telling the compiler - yes I know what I'm doing, its a Dog
! You're not asking permission.
So what would be most useful to me is a stricter version of <Dog> cat
that would prevent extraneous properties:
return <strict State> {
...state,
fullName: action.payload // compiles ok :-(
};
The whole Exact<T>
type thing has many ripple through consequences (this is a long thread!). It reminds me of the whole 'checked exceptions' debate where it's something you think you want but it turns out it has many issues (like suddenly five minutes later wanting an Unexact<T>
).
On the other hand <strict T>
would act more like a barrier to prevent 'impossible' types getting 'through'. It's essentially a type filter that passes through the type (as has been done above with runtime functions).
However it would be easy for newcomers to assume it prevented 'bad data' getting through in cases where it would be impossible for it to do so.
So if I had to make a proposal syntax it would be this:
/// This syntax is ONLY permitted directly in front of an object declaration:
return <strict State> { ...state, foooooooooooooo: 'who' };
Back to the OP: in theory[1] with negated types you could write type Exact<T> = T & not Record<not keyof T, any>
. Then an Exact<{x: string}>
would forbid any types with keys other than x
from being assigned to it. Not sure if that's enough to satisfy what's being asked by everyone here, but it does seem to perfectly fit the OP.
[1] I say in theory because that's predicated on better index signatures as well
Curious to know if I have the issue described here. I have code like:
const Layers = {
foo: 'foo'
bar: 'bar'
baz: 'baz'
}
type Groups = {
[key in keyof Pick<Layers, 'foo' | 'bar'>]: number
}
const groups = {} as Groups
then it allows me to set unknown properties, which is what I don't want:
groups.foo = 1
groups.bar = 2
groups.anything = 2 // NO ERROR :(
Setting anything
still works, and key value type is any
. I was hoping it would be an error.
Is this what will be solved by this issue?
Turns out, I should have been doing
type Groups = {
[key in keyof Pick<typeof Layers, 'foo' | 'bar'>]: number
}
Note the added use of typeof
.
The Atom plugin atom-typescript
was trying hard not to fail, and eventually crashed. When I added typeof
, things went back to normal, and unknown props were no longer allowed which is what I was expecting.
In other words, when I was not using typeof
, atom-typescript
was trying to figure the type in other places of the code where I was using the objects of type Groups
, and it was allowing me to add unknown props and showing me a type hint of any
for them.
So I don't think I have the issue of this thread.
Another complication might be how to handle optional properties.
If you have a type that has optional properties what would Exact<T>
for those properties mean:
export type PlaceOrderResponse = {
status: 'success' | 'paymentFailed',
orderNumber: string
amountCharged?: number
};
Does Exact<T>
mean every optional property must be defined? What would you specify it as? Not 'undefined' or 'null' because that's has a runtime effect.
Does this now require a new way to specify a 'required optional parameter'?
For example what do we have to assign amountCharged
with in the following code sample to get it to satisfy the 'exactness' of the type? We're not being very 'exact' if we don't enforce this property to be at least 'acknowledged' somehow. Is it <never>
? It can't be undefined
or null
.
const exactOrderResponse: Exact<PlaceOrderResponse> =
{
status: 'paymentFailed',
orderNumber: '1001',
amountCharged: ????
};
So you may be thinking - it's still optional, and it is now exactly optional which just translates to optional. And certainly at runtime it would need to not be set, but it looks to me like we just 'broke' Exact<T>
by sticking in a question mark.
Maybe it is only when assigning a value between two types that this check needs to be made? (To enforce that they both include amountCharged?: number
)
Let's introduce a new type here for a dialog box's input data:
export type OrderDialogBoxData = {
status: 'success' | 'paymentFailed',
orderNumber: string
amountCharge?: number // note the typo here!
};
So let's try this out:
// run the API call and then assign it to a dialog box.
const serverResponse: Exact<PlaceOrderResponse> = await placeOrder();
const dialogBoxData: Exact<OrderDialogBoxData> = serverResponse; // SHOULD FAIL
I would expect this to fail of course because of the typo - even though this property is optional in both.
So then I came back to 'Why are we wanting this in the first place?'.
I think it would be for these reasons (or a subset depending upon the situation):
If 'exact optional properties' aren't handled properly then some of these benefits are broken or greatly confused!
Also in the above example we've just 'shoehorned' Exact
in to try to avoid typos but only succeeded in making a huge mess! And it's now even more brittle than ever before.
I think what I often need isn't an actually an Exact<T>
type at all, it is one of these two :
NothingMoreThan<T>
or
NothingLessThan<T>
Where 'required optional' is now a thing. The first allows nothing extra to be defined by the RHS of the assignment, and the second makes sure everything (including optional properties) is specified on the RHS of an assignment.
NothingMoreThan
would be useful for payloads sent across the wire, or JSON.stringify()
and if you were to get an error because you had too many properties on RHS you'd have to write runtime code to select only the needed properties. And that's the right solution - because that's how Javascript works.
NothingLessThan
is kind of what we already have in typescript - for all normal assignments - except it would need to consider optional (optional?: number)
properties.
I don't expect these names to make any traction, but I think the concept is clearer and more granular than Exact<T>
...
Then, perhaps (if we really need it):
Exact<T> = NothingMoreThan<NothingLessThan<T>>;
or would it be:
Exact<T> = NothingLessThan<NothingMoreThan<T>>; // !!
This post is a result of a real problem I'm having today where I have a 'dialog box data type' that contains some optional properties and I want to make sure what's coming from the server is assignable to it.
Final note: NothingLessThan
/ NothingMoreThan
have a similar 'feel' to some of the comments above where type A is extended from type B, or B is extended from A. The limitation there is that they wouldn't address optional properties (at least I don't think they could today).
@simeyla You could just get away with the "nothing more than" variant.
for all T extends X: T
.for all T super X: T
A way to pick one or both explicitly would be sufficient. As a side effect, you could specify Java's T super C
as your proposed T extends NothingMoreThan<C>
. So I'm pretty convinced this is probably better than standard exact types.
I feel this should be syntax though. Maybe this?
extends T
- The union of all types assignable to T, i.e. equivalent to just plain T
.super T
- The union of all types T is assignable to.extends super T
, super extends T
- The union of all types equivalent to T. This just falls out of the grid, since only the type can be both assignable and assigned to itself.type Exact<T> = extends super T
- Sugar built-in for the common case above, to aid readability.This also makes it possible to implement #14094 in userland by just making each variant Exact<T>
, like Exact<{a: number}> | Exact<{b: number}>
.
I wonder if this also makes negated types possible in userland. I believe it does, but I'd need to do some complicated type arithmetic first to confirm that, and it's not exactly an obvious thing to prove.
I wonder if this also makes negated types possible in userland, since (super T) | (extends T) is equivalent to unknown. I believe it is, but I'd need to do some complicated type arithmetic first to confirm that, and it's not exactly an obvious thing to prove.
For (super T) | (extends T) === unknown
to hold assignability would need to be a total order.
@jack-williams Good catch and fixed (by removing the claim). I was wondering why things weren't working out initially when I was playing around a bit.
@jack-williams
"Nothing less than" is just normal types. TS does this implicitly, and every type is treated as equivalent
Yes and no. But mostly yes... ...but only if you're in strict
mode!
So I had a lot of situations where I needed a property to be logically 'optional' but I wanted the compiler to tell me if I had 'forgotten it' or misspelled it.
Well that's exactly what you get with lastName: string | undefined
whereas I had mostly got lastName?: string
, and of course without strict
mode you won't be warned of all the discrepancies.
I've always known about strict mode, and I can't for the life of me find a good reason why I didn't turn it on until yesterday - but now that I have (and I'm still wading through hundreds of fixes) it's much easier to get the behavior I wanted 'out of the box'.
I had been trying all kinds of things to get what I wanted - including playing with Required<A> extends Required<B>
, and trying to remove optional ?
property flags. That sent me down a whole different rabbit hole - (and this was all before I turned strict
mode on).
The point being that if you're trying to get something close to 'exact' types today then you need to start with enabling strict
mode (or whatever combination of flags gives the right checks). And if I needed to add middleName: string | undefined
later then boom - I'd suddenly find everywhere I needed to 'consider it' :-)
PS. thanks for your comments - was very helpful. I'm realizing I've seen A LOT of code that clearly isn't using strict
mode - and then people run into walls like I did. I wonder what can be done to encourage its use more?
@simeyla I think your feedback and thanks should be directed at @isiahmeadows!
I figured I'd write up my experiences with Exact types after implementing a basic prototype. My general thoughts are that the team were spot on with their assessment:
Our hopeful diagnosis is that this is, outside of the relatively few truly-closed APIs, an XY Problem solution.
I don't feel that the cost of introducing yet another object type is repaid by catching more errors, or by enabling new type relationships. Ultimately, exact types let me _say_ more, but they didn't let me _do_ more.
Examining some of the potential uses cases of exact types:
keys
and for ... in
.Having more precise types when enumerating keys seems appealing, but in practice I never found myself enumerating keys for things that were conceptually exact. If you precisely know the keys, why not just address them directly?
The assignability rule { ... } <: { ...; x?: T }
is unsound because the left type may include an incompatible x
property that was aliased away. When assigning from an exact type, this rule becomes sound. In practice I never use this rule; it seems more suited for legacy systems that would not have exact types to begin with.
I had pinned my last hope on exact types improving props passing, and simplification of spread types. The reality is that exact types are the antithesis of bounded polymorphism, and fundamentally non-compositional.
A bounded generic lets you specify props you care about, and pass the rest through. As soon as the bound becomes exact, you completely lose width subtyping and the generic becomes significantly less useful. Another problem is that one of the main tools of composition in TypeScript is intersection, but intersection types are incompatible with exact types. Any non-trivial intersection type with an exact component is going to be vacuous: _exact types do not compose_. For react and props you probably want row types and row polymorphism, but that is for another day.
Almost all the interesting bugs that might be solved by exact types are solved by excess property checking; The biggest problem is that excess property checking does not work for unions without a discriminant property; solve this and almost all of the interesting problems relevant for exact types go away, IMO.
@jack-williams I do agree it's not generally very useful to have exact types. The excess property checking concept is actually covered by my super T
operator proposal, just indirectly because the union of all types T is assignable to notably does not include proper subtypes of T.
I'm not heavily in support of this personally apart from maybe a T super U
*, since about the only use case I've ever encountered for excess property checking were dealing with broken servers, something you can usually work around by using a wrapper function to generate the requests manually and remove the excess garbage. Every other issue I've found reported in this thread so far could be resolved simply by using a simple discriminated union.
* This would basically be T extends super U
using my proposal - lower bounds are sometimes useful for constraining contravariant generic types, and workarounds usually end up introducing a lot of extra type boilerplate in my experience.
@isiahmeadows I certainly agree the lower bounded types can be useful, and if you can get exact types out of that, then that's a win for those that want to use them. I guess I should add a caveat to my post that is: I'm primarily addressing the concept of adding a new operator specifically for exact object types.
@jack-williams I think you missed my nuance that I was primarily referring to the exact types and related part of excess property checking. The bit about lower bounded types was a footnote for a reason - it was a digression that's only tangentially related.
I managed to write an implementation for this that will work for function arguments that require varying degrees of exactness:
// Checks that B is a subset of A (no extra properties)
type Subset<A extends {}, B extends {}> = {
[P in keyof B]: P extends keyof A ? (B[P] extends A[P] | undefined ? A[P] : never) : never;
}
// This can be used to implement partially strict typing e.g.:
// ('b?:' is where the behaviour differs with optional b)
type BaseOptions = { a: string, b: number }
// Checks there are no extra properties (Not More, Less fine)
const noMore = <T extends Subset<BaseOptions, T>>(options: T) => { }
noMore({ a: "hi", b: 4 }) //Fine
noMore({ a: 5, b: 4 }) //Error
noMore({ a: "o", b: "hello" }) //Error
noMore({ a: "o" }) //Fine
noMore({ b: 4 }) //Fine
noMore({ a: "o", b: 4, c: 5 }) //Error
// Checks there are not less properties (More fine, Not Less)
const noLess = <T extends Subset<T, BaseOptions>>(options: T) => { }
noLess({ a: "hi", b: 4 }) //Fine
noLess({ a: 5, b: 4 }) //Error
noLess({ a: "o", b: "hello" }) //Error
noLess({ a: "o" }) //Error |b?: Fine
noLess({ b: 4 }) //Error
noLess({ a: "o", b: 4, c: 5 }) //Fine
// We can use these together to get a fully strict type (Not More, Not Less)
type Strict<A extends {}, B extends {}> = Subset<A, B> & Subset<B, A>;
const strict = <T extends Strict<BaseOptions, T>>(options: T) => { }
strict({ a: "hi", b: 4 }) //Fine
strict({ a: 5, b: 4 }) //Error
strict({ a: "o", b: "hello" }) //Error
strict({ a: "o" }) //Error |b?: Fine
strict({ b: 4 }) //Error
strict({ a: "o", b: 4, c: 5 }) //Error
// Or a fully permissive type (More Fine, Less Fine)
type Permissive<A extends {}, B extends {}> = Subset<A, B> | Subset<B, A>;
const permissive = <T extends Permissive<BaseOptions, T>>(options: T) => { }
permissive({ a: "hi", b: 4 }) //Fine
permissive({ a: 5, b: 4 }) //Error
permissive({ a: "o", b: "hello" }) //Error
permissive({ a: "o" }) //Fine
permissive({ b: 4 }) //Fine
permissive({ a: "o", b: 4, c: 5 }) //Fine
Exact type for variable assignment that I realised doesn't actually do anything...
// This is a little unweildy, there's also a shortform that works in many cases:
type Exact<A extends {}> = Subset<A, A>
// The simpler Exact type works for variable typing
const options0: Exact<BaseOptions> = { a: "hi", b: 4 } //Fine
const options1: Exact<BaseOptions> = { a: 5, b: 4 } //Error
const options2: Exact<BaseOptions> = { a: "o", b: "hello" } //Error
const options3: Exact<BaseOptions> = { a: "o" } //Error |b?: Fine
const options4: Exact<BaseOptions> = { b: 4 } //Error
const options5: Exact<BaseOptions> = { a: "o", b: 4, c: 5 } //Error
// It also works for function typing when using an inline value
const exact = (options: Exact<BaseOptions>) => { }
exact({ a: "hi", b: 4 }) //Fine
exact({ a: 5, b: 4 }) //Error
exact({ a: "o", b: "hello" }) //Error
exact({ a: "o" }) //Error |b?: Fine
exact({ b: 4 }) //Error
exact({ a: "o", b: 4, c: 5 }) //Error
// But not when using a variable as an argument even of the same type
const options6 = { a: "hi", b: 4 }
const options7 = { a: 5, b: 4 }
const options8 = { a: "o", b: "hello" }
const options9 = { a: "o" }
const options10 = { b: 4 }
const options11 = { a: "o", b: 4, c: 5 }
exact(options6) //Fine
exact(options7) //Error
exact(options8) //Error
exact(options9) //Error |b?: Fine
exact(options10) //Error
exact(options11) //Fine -- Should not be Fine
// However using strict does work for that
// const strict = <T extends Strict<BaseOptions, T>>(options: T) => { }
strict(options6) //Fine
strict(options7) //Error
strict(options8) //Error
strict(options9) //Error |b?: Fine
strict(options10) //Error
strict(options11) //Error -- Is correctly Error
https://www.npmjs.com/package/ts-strictargs
https://github.com/Kotarski/ts-strictargs
I feel like I have a use case for this when wrapping React components, where I need to "pass through" props: https://github.com/Microsoft/TypeScript/issues/29883. @jack-williams Any thoughts on this?
@OliverJAsh Looks relevant, but I must admit I don't know React as well as most. I guess it would be helpful to work through how exact types can precisely help here.
type MyComponentProps = { foo: 1 };
declare const MyComponent: ComponentType<MyComponentProps>;
type MyWrapperComponent = MyComponentProps & { myWrapperProp: 1 };
const MyWrapperComponent: ComponentType<MyWrapperComponent> = props => (
<MyComponent
// We're passing too many props here, but no error!
{...props}
/>
);
Please correct me at any point I say something wrong.
I'm guessing the start would be to specify MyComponent
to accept an exact type?
declare const MyComponent: ComponentType<Exact<MyComponentProps>>;
In that case then we would get an error, but how do you fix the error? I'm assuming here that the wrapper components don't just have the same prop type all the way down, and at some point you really do need to dynamically extract a prop subset. Is this a reasonable assumption?
If MyWrapperComponent
props is also exact then I think it would be sufficient to do a destructuring bind. In the generic case this would require an Omit
type over an exact type, and I really don't know the semantics there. I'm guessing it could work like a homomorphic mapped type and retain the exact-ness, but I think this would require more thought.
If MyWrapperComponent
is not exact then it will require some run-time check to prove the exactness of the new type, which can only be done by explicitly selecting the properties you want (which doesn't scale as you say in your OP). I'm not sure how much you gain in this case.
Things that I haven't covered because I don't know how likely they are is the generic case, where props
is some generic type, and where you need to combine props like { ...props1, ...props2 }
. Is this common?
@Kotarski Did you publish it by any chance in NPM registry?
@gitowiec
@Kotarski Did you publish it by any chance in NPM registry?
https://www.npmjs.com/package/ts-strictargs
https://github.com/Kotarski/ts-strictargs
I have this use-case:
type AB = { a: string, b: string }
type CD = { c: string, d: string }
type ABCD = AB & CD
// I want this to error, because the 'c' should mean it prevents either AB or ABCD from being satisfied.
const foo: AB | ABCD = { a, b, c };
// I presume that I would need to do this:
const foo: Exact<AB> | Exact<ABCD> = { a, b, c };
@ryami333 That does not need exact types; that just needs a fix to excess property checking: #13813.
@ryami333 If you are willing to use an extra type, I have a type that will do what you want it to, namely force a stricter version of unions :
type AB = { a: string, b: string }
type CD = { c: string, d: string }
type ABCD = AB & CD
type UnionKeys<T> = T extends any ? keyof T : never;
type StrictUnionHelper<T, TAll> = T extends any ? T & Partial<Record<Exclude<UnionKeys<TAll>, keyof T>, never>> : never;
type StrictUnion<T> = StrictUnionHelper<T, T>
// Error now.
const foo: StrictUnion<AB | ABCD> = { a: "", b: "", c: "" };
@dragomirtitian Fascinating. It's curious to me why
type KeyofV1<T extends object> = keyof T
produces a different result than
type KeyofV2<T> = T extends object ? keyof T : never
Could someone explain this to me?
type AB = { a: string, b: string }
type CD = { c: string, d: string }
type ABCD = AB & CD
KeyofV1< AB | ABCD > // 'a' | 'b'
KeyofV2< AB | ABCD > // 'a' | 'b' | 'c' | 'e'
V1
gets the common keys of the union, V2
gets the keys of each union member and unions the result.
@weswigham Is there a reason they should be returning different results?
Yes? As I said - V1
gets the _common keys_ to every union member, because the argument to keyof
ends up being keyof (AB | ABCD)
, which is just "A" | "B"
, while the version within the conditional only receives one union member at a time, thanks to the conditional distributing over its input, so it's essentially keyof AB | keyof ABCD
.
@weswigham So the conditional evaluates it more like this, like via some implicit loop?
type Union =
(AB extends object ? keyof AB : never) |
(ABCD extends object ? keyof ABCD : never)
When I'm reading that code, I'd normally expect the (AB | ABCD) extends object
check to operate as a single unit, checking that (AB | ABCD)
is assignable to object
, then it returning keyof (AB | ABCD)
as a unit, 'a' | 'b'
. The implicit mapping seems really strange to me.
@isiahmeadows You can look at distributive conditional types as a foreach for unions. They apply the conditional type to each member of the union in turn and the result is the union of each partial result.
So UnionKeys<A | B> = UnionKeys<A> | UnionKeys<B> =(keyof A) | (keyof B)
But only if the conditional type distributes, and it distributes only if the tested type is a naked type parameter. So:
type A<T> = T extends object ? keyof T : never // distributive
type B<T> = [T] extends [object] ? keyof T : never // non distributive the type parameter is not naked
type B<T> = object extends T ? keyof T : never // non distributive the type parameter is not the tested type
Thanks guys, I think I got it. I re-arranged it for my understanding; I believe that the NegativeUncommonKeys
is useful on its own as well. Here it is in case it is useful to someone else as well.
type UnionKeys<T> = T extends any ? keyof T : never;
type NegateUncommonKeys<T, TAll> = (
Partial<
Record<
Exclude<
UnionKeys<TAll>,
keyof T
>,
never
>
>
)
type StrictUnion<T, TAll = T> = T extends any
? T & NegateUncommonKeys<T, TAll>
: never;
I also understand why T
and TAll
are both there. The "loop effect", where T is tested and naked, means that each item in the union for T
is applied whereas the untested TAll
contains the original and complete union of all items.
This is the handbook segment on distributive conditional types.
@weswigham Yeah .. except I feel that section reads like it was written by one compiler engineer for another compiler engineer.
Conditional types in which the checked type is a naked type parameter are called distributive conditional types.
What are naked type parameters ? (and why don't they put some clothes on 😄)
i.e. T refers to the individual constituents after the conditional type is distributed over the union type)
Just yesterday I had a discussion about what this particular sentence means and why there was an emphasis on the word 'after'.
I think the documentation is written assuming prior knowledge and terminology that users might not always have.
The handbook section does make sense to me and it explains it much better, but I still am skeptical of the design choice there. It just doesn't logically make sense to me how that behavior would naturally follow from a set theoretic and type-theoretic perspective. It just comes across as a little too hackish.
naturally follow from a set theoretic and type-theoretic perspective
Take each item in a set and partition it according to a predicate.
That's a distributive operation!
Take each item in a set and partition it according to a predicate.
Although that only makes sense when you're talking about sets of sets (ie, a union type) which starts sounding an awful lot more like category theory.
@RyanCavanaugh Okay, so let me clarify: I intuitively read T extends U ? F<T> : G<T>
as T <: U ⊢ F(T), (T <: U ⊢ ⊥) ⊢ G(T)
, with the comparison done not piecewise, but as a complete step. That's distinctly different from "the union of for all {if t ∈ U then F({t}) else G({t}) | t ∈ T}
, which is what's currently the semantics.
(Pardon if my syntax is a bit off - my type theory knowledge is entirely self-taught, so I know I don't know all the syntactic formalisms.)
Which operation is more intuitive is up for infinite debate, but with the current rules it's easy to make a distributive type non-distributive with [T] extends [C]
. If the default were non-distributive, you'd need some new incantation at a different level to cause distributivity. That's also a separate question from which behavior is more often preferred; IME I almost never want a non-distributing type.
Ye there is no strong theoretical grounding for distribution because it’s a syntactic operation.
The reality is that it is very useful and trying to encode it some other way would be painful.
As it stands, I'll go ahead and trail off before I drive the conversation too far off topic.
there are so many issues about distrubutivness already, why won't we face it that new syntax is required?
Here is an example problem:
I want to specify that my users API endpoint/serice must NOT return any extra properties (like e.g. password) other than the ones specified in the service interface. If I accidentally return an object with extra properties, I want a compile time error, regardless of whether the result object has been produced by an object literal or otherwise.
A run time check of every returned object can be costly, especially for arrays.
Excess property checking doesn't help in this case. Honestly, I think its a wonky one-trick-pony solution. In theory it should've provided an "it just works" kind of experience - in practice its also a source of confusion Exact object types should have been implemented instead, they would have covered both use cases nicely.
@babakness Your type NoExcessiveProps
is a no-op. I think they mean something like this:
interface API {
username: () => { username: string }
}
const api: API = {
username: (): { username: string } => {
return { username: 'foobar', password: 'secret'} // error, ok
}
}
const api2: API = {
username: (): { username: string } => {
const id: <X>(x: X) => X = x => x;
const value = id({ username: 'foobar', password: 'secret' });
return value // no error, bad?
}
}
As the writer of the API type you want to enforce that username
just returns the username, but any implementer can get around that because object types have no width restriction. That can only be applied at the initialisation of a literal, which the implementer may, or may not, do. Though, I would heavily discourage anyone from trying to use exact types as language based security.
@spion
Excess property checking doesn't help in this case. Honestly, I think its a wonky one-trick-pony solution. In theory they should've provided an "it just works" kind of experience
EPC is a reasonably sensible and lightweight design choice that covers are large set of problem. The reality is that Exact types do not 'just work'. To implement in a sound way that supports extensibility requires a completely different type system.
@jack-williams Of course there would be other ways to verify present as well (runtime checks where performance is not an issue, tests etc) but an additional compile-time one is invaluable for fast feedback.
Also, I didn't mean that exact types "just work". I meant that EPC was meant to "just work" but in practice its just limited, confusing and unsafe. Mainly because if try to "deliberately" use it you generally end up shooting yourself in the foot.
edit: Yep, I edited to replace "they" with "it" as I realized its confusing.
@spion
Also, I didn't mean that exact types "just work". I meant that EPC was meant to "just work" but in practice its just limited, confusing and unsafe. Mainly because if try to "deliberately" use it you generally end up shooting yourself in the foot.
My mistake. Read the original comment as
In theory they should've provided an "it just works" kind of experience [which would have been exact types instead of EPC]
commentary in [] being my reading.
The revised statement:
In theory it should've provided an "it just works" kind of experience
is much clearer. Sorry for my misinterpretation!
type NoExcessiveProps<O> = {
[K in keyof O]: K extends keyof O ? O[K] : never
}
// no error
const getUser1 = (): {username: string} => {
const foo = {username: 'foo', password: 'bar' }
return foo
}
// Compile-time error, OK
const foo: NoExcessiveProps<{username: string}> = {username: 'a', password: 'b' }
// No error? 🤔
const getUser2 = (): NoExcessiveProps<{username: string}> => {
const foo = {username: 'foo', password: 'bar' }
return foo
}
The result for getUser2
is surprising, it feels inconsistent and like it should produce a compile-time error. Whats the insight on why it doesn't?
@babakness Your NoExcessiveProps
just evaluates back to T
(well a type with the same keys as T
). In [K in keyof O]: K extends keyof O ? O[K] : never
, K
will always be a key of O
since you are mapping over keyof O
. Your const
example errors because it triggers EPC just as it would have if you would have typed it as {username: string}
.
If you don't mind calling an extra function we can capture the actual type of the object passed in, and do a custom form of excess property checks. (I do realize the whole point is to automatically catch this type of error, so this might be of limited value):
function checked<T extends E, E>(o: T & Record<Exclude<keyof T, keyof E>, never>): E {
return o;
}
const getUser2 = (): { username: string } => {
const foo = { username: 'foo', password: 'bar' }
return checked(foo) //error
}
const getUser3 = (): { username: string } => {
const foo = { username: 'foo' }
return checked(foo) //ok
}
@dragomirtitian Ah... right... good point! So I'm trying to understand your checked
function. I'm particularly puzzled
const getUser2 = (): { username: string } => {
const foo = { username: 'foo', password: 'bar' }
const bar = checked(foo) // error
return checked(foo) //error
}
const getUser3 = (): { username: string } => {
const foo = { username: 'foo' }
const bar = checked(foo) // error!?
return checked(foo) //ok
}
The bar
assignment in getUser3
fails. The error seems to be at foo
Details of the error
The type for bar
here is {}
, which seems as though it is because on checked
function checked<T extends E, E>(o: T & Record<Exclude<keyof T, keyof E>, never>): E {
return o;
}
E
is not assigned anywhere. Yet if we replace typeof E
with typeof {}
, it doesn't work.
What is the type for E? Is there some kind of context-aware thing happening?
@babakness If there is no other place to infer a type parameter from, typescript will infer it from the return type. So when we are assigning the result of checked
to the return of getUser*
, E
will be the return type of the function, and T
will be the actual type of the value you want to return. If there is no place to infer E
from it will just default to {}
and so you will always get an error.
The reason I did it like this was to avoid any explicit type parameters, you could create a more explicit version of it:
function checked<E>() {
return function <T extends E>(o: T & Record<Exclude<keyof T, keyof E>, never>): E {
return o;
}
}
const getUser2 = (): { username: string } => {
const foo = { username: 'foo', password: 'bar' }
return checked<{ username: string }>()(foo) //error
}
const getUser3 = (): { username: string } => {
const foo = { username: 'foo' }
return checked<{ username: string }>()(foo) //ok
}
Note: The curried function approach is necessary since we don't yet have partial argument inference (https://github.com/Microsoft/TypeScript/pull/26349) so we can't specify some type parameter and have others inferred in the same call. To get around this we specify E
in the first call and let T
be inferred in the second call. You could also cache the cache
function for a specific type and use the cached version
function checked<E>() {
return function <T extends E>(o: T & Record<Exclude<keyof T, keyof E>, never>): E {
return o;
}
}
const checkUser = checked<{ username: string }>()
const getUser2 = (): { username: string } => {
const foo = { username: 'foo', password: 'bar' }
return checkUser(foo) //error
}
const getUser3 = (): { username: string } => {
const foo = { username: 'foo' }
return checkUser(foo) //ok
}
FWIW this is a WIP / sketch tslint rule that solves the specific problem of not accidentally returning extra properties from "exposed" methods.
https://gist.github.com/spion/b89d1d2958f3d3142b2fe64fea5e4c32
For the spread use case – see https://github.com/Microsoft/TypeScript/issues/12936#issuecomment-300382189 – could a linter detect a pattern like this and warn that it's not type-safe?
Copying code example from the aforementioned comment:
interface State {
name: string;
}
function nameReducer(state: State, action: Action<string>): State {
return {
...state,
fullName: action.payload // compiles, but it's an programming mistake
}
}
cc @JamesHenry / @armano2
Would very much like to see that happen. We use generated TypeScript definitions for GraphQL endpoints and it's a problem that TypeScript does not raise an error when I pass an object with more fields than necessary to a query because GraphQL will fail to execute such a query at runtime.
how much of this is now addressed with the 3.5.1 update w/ better checking for extra properties during assignment? we got a bunch of known problem areas flagged as errors the way we wanted them to be after upgrading to 3.5.1
if you have a problem and you think exact types are the right solution, please describe the original problem here
https://github.com/microsoft/TypeScript/issues/12936#issuecomment-284590083
Here's one involving React refs: https://github.com/microsoft/TypeScript/issues/31798
/cc @RyanCavanaugh
One use case for me is
export const mapValues =
<T extends Exact<T>, V>(object: T, mapper: (value: T[keyof T], key: keyof T) => V) => {
type TResult = Exact<{ [K in keyof T]: V }>;
const result: Partial<TResult> = { };
for (const [key, value] of Object.entries(object)) {
result[key] = mapper(value, key);
}
return result as TResult;
};
This is unsound if we don't use exact types, since if object
has extra properties, it's not safe to call mapper
on those extra keys and values.
The real motivation here is that I want to have the values for an enum somewhere that I can reuse in the code:
const choices = { choice0: true, choice1: true, choice2: true };
const callbacksForChoices = mapValues(choices, (_, choice) => () => this.props.callback(choice));
where this.props.callback
has type (keyof typeof choices) => void
.
So really it's about the type system being able to represent the fact that I have a list of keys in code land that exactly matches a set (e.g., a union) of keys in type land, so that we can write functions that operate on this list of keys and make valid type assertions about the result. We can't use an object (choices
in my previous example) because as far as the type system knows, the code-land object could have extra properties beyond whatever object type is used. We can't use an array (['choice0', 'choice1', 'choice2'] as const
, because as far as the type system knows, the array might not contain all of the keys allowed by the array type.
Maybe exact
shouldn't be a type, but only a modifier on function's inputs and/or output? Something like flow's variance modifier (+
/-
)
I want to add on to what @phaux just said. The real use I have for Exact
is to have the compiler guarantee the shape of functions. When I have a framework, I may want either of these: (T, S): AtMost<T>
, (T, S): AtLeast<T>
, or (T, S): Exact<T>
where the compiler can verify that the functions a user defines will fit exactly.
Some useful examples:
AtMost
is useful for config (so we don't ignore extra params/typos and fail early).
AtLeast
is great for things like react components and middleware where a user may shove whatever extra they want onto an object.
Exact
is useful for serialisation/deserialization (we can guarantee we don't drop data and these are isomorphic).
Would this help to prevent this from happening?
interface IDate {
year: number;
month: number;
day: number;
}
type TBasicField = string | number | boolean | IDate;
// how to make this generic stricter?
function doThingWithOnlyCorrectValues<T extends TBasicField>(basic: T): void {
// ... do things with basic field of only the exactly correct structures
}
const notADate = {
year: 2019,
month: 8,
day: 30,
name: "James",
};
doThingWithOnlyCorrectValues(notADate); // <- this should not work! I want stricter type checking
We really need a way in TS to say T extends exactly { something: boolean; } ? xxx : yyy
.
Or otherwise, something like:
const notExact = {
something: true,
name: "fred",
};
Will still return xxx
there.
Maybe const
keyword can be used? e.g.T extends const { something: boolean }
@pleerock it might be slightly ambiguous, as in JavaScript / TypeScript we can define a variable as const
but still add / remove object properties. I think the keyword exact
is pretty to the point.
I'm not sure if it's exactly related, but i'd expect at least two errors in this case:
playground
@mityok I think that is related. I'm guessing you would like to do something along the lines of:
class Animal {
makeSound(): exact Foo {
return { a: 5 };
}
}
If the exact
made the type stricter - then it shouldn't be extendable with an extra property, as you've done in Dog
.
taking advantage of the const
(as const
) and using before interfaces and types, like
const type WillAcceptThisOnly = number
function f(accept: WillAcceptThisOnly) {
}
f(1) // error
f(1 as WillAcceptThisOnly) // ok, explicit typecast
const n: WillAcceptThisOnly = 1
f(n) // ok
would be really verbose having to assign to const variables, but would avoid a lot of edge cases when you pass a typealias that wasn't exact what you were expecting
I have came up with pure TypeScript solution for Exact<T>
problem that, I believe, behaves exactly like what has been requested in the main post:
// (these two types MUST NOT be merged into a single declaration)
type ExactInner<T> = <D>() => (D extends T ? D : D);
type Exact<T> = ExactInner<T> & T;
function exact<T>(obj: Exact<T> | T): Exact<T> {
return obj as Exact<T>;
};
The reason ExactInner
must be not included in the Exact
is due to #32824 fix not being released yet (but already merged in !32924).
It's only possible to assign a value to the variable or function argument of type Exact<T>
, if the right hand expression is also Exact<T>
, where T
is exactly identical type in both parts of assignment.
I haven't achieved automatic promotion of values into Exact types, so that's what exact()
helper function is for. Any value can be promoted to be of exact type, but assignment will only succeed if TypeScript can prove that underlying types of both parts of expression are not just extensible, but exactly the same.
It works by exploiting the fact that TypeScript uses extend
relation check to determine if right hand type can be assigned to the left hand type — it only can if right hand type (source) _extends_ the left hand type (destination).
Quoting checker.ts
,
// Two conditional types 'T1 extends U1 ? X1 : Y1' and 'T2 extends U2 ? X2 : Y2' are related if
// one of T1 and T2 is related to the other, U1 and U2 are identical types, X1 is related to X2,
// and Y1 is related to Y2.
ExactInner<T>
generic uses the described approach, substituting U1
and U2
with underlying types that require exactness checks. Exact<T>
adds an intersection with plain underlying type, which allows TypeScript to relax exact type when its target variable or function argument are not an exact type.
From programmer's perspective, Exact<T>
behaves as if it sets an exact
flag on T
, without inspecting T
or changing it, and without creating an independent type.
Here are playground link and gist link.
Possible future improvement would be to allow auto-promotion of non-exact types into exact types, completely removing the need in exact()
function.
Amazing work @toriningen!
If anyone is able to find a way to make this work without having to wrap your value in a call to exact
it would be perfect.
Not sure if this is the right issue, but here is an example of something I'd like to work.
https://www.typescriptlang.org/play/#code/KYOwrgtgBAyg9gJwC4BECWDgGMlriKAbwCgooBBAZyygF4oByAQ2oYBpSoVhq7GATHlgbEAvsWIAzMCBx4CTfvwDyCQQgBCATwAU-DNlz4AXFABE5GAGEzUAD7mUAUWtmAlEQnjiilWuCauvDI6Jhy+AB0VFgRSHAAqgAOiQFWLMA6bm4A3EA
enum SortDirection {
Asc = 'asc',
Desc = 'desc'
}
function addOrderBy(direction: "ASC" | "DESC") {}
addOrderBy(SortDirection.Asc.toUpperCase());
@lookfirst That's different. This is asking for a feature for types that don't admit extra properties, like some type exact {foo: number}
where {foo: 1, bar: 2}
isn't assignable to it. That's just asking for text transforms to apply to enum values, which likely doesn't exist.
Not sure if this is the right issue, but [...]
In my experience as a maintainer elsewhere, if you're in doubt and couldn't find any clear existing issue, file a new bug and worst case scenario, it gets closed as a dupe you didn't find. This is pretty much the case in most major open source JS projects. (Most of us bigger maintainers in the JS community are actually decent people, just people who can get really bogged down over bug reports and such and so it's hard not to be really terse at times.)
@isiahmeadows Thanks for the response. I didn't file a new issue because I was searching for duplicate issues first, which is the correct thing to do. I was trying to avoid bogging people down because I wasn't sure if this was the right issue or not or even how to categorize what I was talking about.
Another sample use case:
https://www.typescriptlang.org/play/index.html#code/JYOwLgpgTgZghgYwgAgIoFdoE8AKcpwC2EkUAzsgN4BQydycAXMmWFKAOYDct9ARs1bsQ3agF9q1GOhAIwwAPYhkCKBDiQAKgAtgUACZ4oYXHA4QAqgCUAMgAoAjpiimChMswzYjREtDIAlFS8dGpg6FDKAAbaYGAADh4A9EkQAB5E8QA2EAB0CAqESQD8ACSUIBAA7sjWNgDK6lAI2j7udgDyfABWEHK5EODsEGSOzq5EgQFiUeKSKcgAolBQCuQANAwU6fF9kPrUqupaugZGJnjmdXaUDMwA5PebAsiPmwgPYLpkALTx+EQflVgFksj8EHB0GQID8vnp9H98CZEeZYQpwQQyNp7sgxAEeIclKxkP83BQALxUO6vJ7IF5vFSfb6ItxAkFgiFQmFwgws5H-VFgdGqOBYnFiAkLQC8G4BGPeQJl2Km0fQA1pxkPoIDBjhB9HSsFsyMAOCB1cAwPKFAwSQDiKRDmoNBAdPDzqYrrY7KTJvigA
EDITED: Check @aigoncharov solution bellow, because I think is even faster.
type Exact<T, R> = T extends R
? R extends T
? T
: never
: never
Don't know if this can be improved more.
type Exact<T, Shape> =
// Check if `T` is matching `Shape`
T extends Shape
// Does match
// Check if `T` has same keys as `Shape`
? Exclude<keyof T, keyof Shape> extends never
// `T` has same keys as `Shape`
? T
// `T` has more keys than `Shape`
: never
// Does not match at all
: never;
type InexactType = {
foo: string
}
const obj = {
foo: 'foo',
bar: 'bar'
}
function test1<T>(t: Exact<T, InexactType>) {}
function test2(t: InexactType) {}
test1(obj) // $ExpectError
test2(obj)
Without comments
type ExactKeys<T1, T2> = Exclude<keyof T1, keyof T2> extends never
? T1
: never
type Exact<T, Shape> = T extends Shape
? ExactKeys<T, Shape>
: never;
Don't know if this can be improved more.
type Exact<T, Shape> = // Check if `T` is matching `Shape` T extends Shape // Does match // Check if `T` has same keys as `Shape` ? Exclude<keyof T, keyof Shape> extends never // `T` has same keys as `Shape` ? T // `T` has more keys than `Shape` : never // Does not match at all : never; type InexactType = { foo: string } const obj = { foo: 'foo', bar: 'bar' } function test1<T>(t: Exact<T, InexactType>) {} function test2(t: InexactType) {} test1(obj) // $ExpectError test2(obj)
Without comments
type ExactKeys<T1, T2> = Exclude<keyof T1, keyof T2> extends never ? T1 : never type Exact<T, Shape> = T extends Shape ? ExactKeys<T, Shape> : never;
Love that idea!
Another trick that could do the job is to check assignability in both directions.
type Exact<T, R> = T extends R
? R extends T
? T
: never
: never
type A = {
prop1: string
}
type B = {
prop1: string
prop2: string
}
type C = {
prop1: string
}
type ShouldBeNever = Exact<A, B>
type ShouldBeA = Exact<A, C>
http://www.typescriptlang.org/play/#code/C4TwDgpgBAogHgQwMbADwBUA0UBKA+KAXinSgjmAgDsATAZ1wCgooB+XMi6+k5lt3vygAuKFQgA3CACc+o8VNmNQkKAEEiUAN58w0gPZgAjKLrBpASyoBzRgF9l4aACFNOlnsMmoZyzd0GYABMpuZWtg4q0ADCbgFeoX4RjI6qAMoAFvoArgA2NM4QAHKSMprwyGhq2M54qdCZOfmFGsQVKKjVUNF1QkA
Another playground from @iamandrewluca https://www.typescriptlang.org/play/?ssl=7&ssc=6&pln=7&pc=17#code/C4TwDgpgBAogHgQwMbADwBUA0UBKA+KAXinSgjmAgDsATAZ1wCgooB+XMi6+k5lt3vygAuKFQgA3CACc+o8VNmNQkKAElxiFOnDRiAbz4sAZgHtTousGkBLKgHNGAX0aMkpqlaimARgCsiKEMhMwsoAHJQ8MwjKB8EaVFw+Olw51djAFcqFBsPKEorAEYMPAAKYFF4ZDQsdU0anUg8AEoglyyc4DyqAogrACYK0Q1yRt02-RdlfuAist8-NoB6ZagAEnhIFBhpaVNZQuAhxZagA
A nuance here is whether Exact<{ prop1: 'a' }>
should be assignable to Exact<{ prop1: string }>
. In my use cases, it should.
@jeremybparagon your case is covered. Here are some more cases.
type InexactType = {
foo: 'foo'
}
const obj = {
// here foo is infered as `string`
// and will error because `string` is not assignable to `"foo"`
foo: 'foo',
bar: 'bar'
}
function test1<T>(t: Exact<T, InexactType>) {}
function test2(t: InexactType) {}
test1(obj) // $ExpectError
test2(obj) // $ExpectError
type InexactType = {
foo: 'foo'
}
const obj = {
// here we cast to `"foo"` type
// and will not error
foo: 'foo' as 'foo',
bar: 'bar'
}
function test1<T>(t: Exact<T, InexactType>) {}
function test2(t: InexactType) {}
test1(obj) // $ExpectError
test2(obj)
type Exact<T, R> = T extends R
? R extends T
? T
: never
: never
I think anybody using this trick (and I'm not saying there aren't valid uses for it) should be acutely aware that it is very very easy to get more props in the "exact" type. Since InexactType
is assignable to Exact<T, InexactType>
if you have something like this, you break out of exactness without realizing it:
function test1<T>(t: Exact<T, InexactType>) {}
function test2(t: InexactType) {
test1(t); // inexactType assigned to exact type
}
test2(obj) // but
This is the reason (at least one of them) that TS does not have exact types, as it would require a complete forking of object types in exact vs non-exact types where an inexact type is never assignable to an exact one, even if at face value they are compatible. The inexact type may always contain more properties. (At least this was one of the reasons @ahejlsberg mentioned as tsconf).
If asExact
were some syntactic way of marking such an exact object, this is what such a solution might look like:
declare const exactMarker: unique symbol
type IsExact = { [exactMarker]: undefined }
type Exact<T extends IsExact & R, R> =
Exclude<keyof T, typeof exactMarker> extends keyof R? T : never;
type InexactType = {
foo: string
}
function asExact<T>(o: T): T & IsExact {
return o as T & IsExact;
}
const obj = asExact({
foo: 'foo',
});
function test1<T extends IsExact & InexactType>(t: Exact<T, InexactType>) {
}
function test2(t: InexactType) {
test1(t); // error now
}
test2(obj)
test1(obj); // ok
const obj2 = asExact({
foo: 'foo',
bar: ""
});
test1(obj2);
const objOpt = asExact < { foo: string, bar?: string }>({
foo: 'foo',
bar: ""
});
test1(objOpt);
@dragomirtitian that's why I came up with the solution a bit earlier https://github.com/microsoft/TypeScript/issues/12936#issuecomment-524631270 that doesn't suffer from this.
@dragomirtitian it's a matter of how you type your functions.
If you do it a little differently, it works.
type Exact<T, R> = T extends R
? R extends T
? T
: never
: never
type InexactType = {
foo: string
}
const obj = {
foo: 'foo',
bar: 'bar'
}
function test1<T>(t: Exact<T, InexactType>) {}
function test2<T extends InexactType>(t: T) {
test1(t); // fails
}
test2(obj)
https://www.typescriptlang.org/play/#code/C4TwDgpgBAogHgQwMbADwBUA0UBKA+KAXinSgjmAgDsATAZ1wCgooB+XMi6+k5lt3vygAuKFQgA3CACc+o8VNmNQkKAElxiFOnDRiAbz4sAZgHtTousGkBLKgHNGAX0aMkpqlaimARgCsiKEMhMwsoAHJQ8MwjKB8EaVFw+Olw51djAFcqFBsPKEorAEYMPAAKYFF4ZDQsdU0anUg8AEogl0YsnOA8qgKIKwAmDE5KWgYNckbdcsqSNuD+4oqWgG4oAHoNqGMEGwAbOnTC4EGy3z82oA
@jeremybparagon your case is covered.
@iamandrewluca I think the solutions here and here differ on how they treat my example.
type Exact<T, R> = T extends R
? R extends T
? T
: never
: never
type A = {
prop1: 'a'
}
type C = {
prop1: string
}
type ShouldBeA = Exact<A, C> // This evaluates to never.
const ob...
@aigoncharov The problem is you need to be aware of that so one could easily not do this and test1
could still get called with extra properties. IMO any solution that can so easily allow an accidental inexact assignment has already failed as the whole point is to enforce exactness in the type system.
@toriningen yeah your solution seems better, I was just referring to the last posted solution. Your solution has going for it the fact that you don't need the extra function type parameter, however it does not seem to work well for optional properties:
// (these two types MUST NOT be merged into a single declaration)
type ExactInner<T> = <D>() => (D extends T ? D : D);
type Exact<T> = ExactInner<T> & T;
type Unexact<T> = T extends Exact<infer R> ? R : T;
function exact<T>(obj: Exact<T> | T): Exact<T> {
return obj as Exact<T>;
};
////////////////////////////////
// Fixtures:
type Wide = { foo: string, bar?: string };
type Narrow = { foo: string };
type ExactWide = Exact<Wide>;
type ExactNarrow = Exact<Narrow>;
const ew: ExactWide = exact<Wide>({ foo: "", bar: ""});
const assign_en_ew: ExactNarrow = ew; // Ok ?
@jeremybparagon I'm not sure @aigoncharov 's solution does a good job on optional properties though. Any solution based on T extends S
and S extends T
will suffer from the simple fact that
type A = { prop1: string }
type C = { prop1: string, prop2?: string }
type CextendsA = C extends A ? "Y" : "N" // Y
type AextendsC = A extends C ? "Y" : "N" // also Y
I think @iamandrewluca of using Exclude<keyof T, keyof Shape> extends never
is good, my type is quite similar (I edited my original answer to add the &R
to ensure T extends R
without any extra checks).
type Exact<T extends IsExact & R, R> =
Exclude<keyof T, typeof exactMarker> extends keyof R? T : never;
I would not stake my reputation that my solution does not have holes though, I haven't looked that hard for them but welcome any such findings 😊
we should have a flag where this is enabled globally. In this way, who wants to loose type can keep doing the same. Way too many bugs caused by this issue. Now I try to try to avoid spread operator and use pickKeysFromObject(shipDataRequest, ['a', 'b','c'])
Here's a use case for exact types I recently stumbled on:
type PossibleKeys = 'x' | 'y' | 'z';
type ImmutableMap = Readonly<{ [K in PossibleKeys]?: string }>;
const getFriendlyNameForKey = (key: PossibleKeys) => {
switch (key) {
case 'x':
return 'Ecks';
case 'y':
return 'Why';
case 'z':
return 'Zee';
}
};
const myMap: ImmutableMap = { x: 'foo', y: 'bar' };
const renderMap = (map: ImmutableMap) =>
Object.keys(map).map(key => {
// Argument of type 'string' is not assignable to parameter of type 'PossibleKeys'
const friendlyName = getFriendlyNameForKey(key);
// No index signature with a parameter of type 'string' was found on type 'Readonly<{ x?: string | undefined; y?: string | undefined; z?: string | undefined; }>'.
return [friendlyName, map[key]];
});
;
Because types are inexact by default, Object.keys
has to return a string[]
(see https://github.com/microsoft/TypeScript/pull/12253#issuecomment-263132208), but in this case, if ImmutableMap
was exact, there's no reason it couldn't return PossibleKeys[]
.
@dallonf note that this example requires extra functionality besides just exact types -- Object.keys
is just a function and there'd need to be some mechanism for describing a function that returns keyof T
for exact types and string
for other types. Simply having the option to declare an exact type wouldn't be sufficient.
@RyanCavanaugh I think that was the implication, exact types + the ability to detect them.
Use case for the react typings:
forwardRef<T, P>(render: (props: P, ref: Ref<T>) => ReactElement<P> & { displayName?: string }) => ComponentType<P>
.
It's tempting to pass a regular component to forwardRef
which is why React issues runtime warnings if it detects propTypes
or defaultProps
on the render
argument. We'd like to express this at the type level but have to fallback to never
:
- forwardRef<T, P>(render: (props: P, ref: Ref<T>) => ReactElement<P> & { displayName?: string }) => ComponentType<P>
+ forwardRef<T, P>(render: (props: P, ref: Ref<T>) => ReactElement<P> & { displayName?: string, propTypes?: never, defaultProps?: never }) => ComponentType<P>
The error message with never
is not helpful ("{} is not assignable to undefined").
Can someone help me out on how @toriningen's solution would look like with a union of different event object shapes? I want to restrict my event shapes in redux-dispatch calls, e.g.:
type StoreEvent =
| { type: 'STORE_LOADING' }
| { type: 'STORE_LOADED'; data: unknown[] }
It's unclear how I could make a typed dispatch() function which only accepts the exact shape of an event.
(UPDATE: I figured it out: https://gist.github.com/sarimarton/d5d539f8029c01ca1c357aba27139010)
Use case:
Missing Exact<>
support leads to runtime problems with GraphQL mutations. GraphQL accepts exact list of permitted properties. If you provide excessive props, it throws an error.
So when we obtain some data from the form, then Typescript cannot validate excess (extra) properties. And we will get an error at runtime.
The following example illustrates imaginary safety
According to the article https://fettblog.eu/typescript-match-the-exact-object-shape/ and similar solutions provided above we can use the following ugly solution:
savePerson<T>(person: ValidateShape<T, Person>)
solution is Ugly?Assume you have deeply nested input type eg.:
// Assume we are in the ideal world where implemented Exact<>
type Person {
name: string;
address: Exact<Address>;
}
type Address {
city: string
location: Exact<Location>
}
type Location {
lon: number;
lat: number;
}
savePerson(person: Exact<Person>)
I cannot imagine what spaghetti we should write to get the same behavior with the currently available solution:
savePerson<T, TT, TTT>(person:
ValidateShape<T, Person keyof ...🤯...
ValidateShape<TT, Address keyof ...💩...
ValidateShape<TTT, Location keyof ...🤬...
> > >)
So, for now, we have big holes in static analysis in our code, which works with complex nested input data.
The case described in the first image, where TS doesn't validate excess properties because "freshness" is lost, has also been a bit of a pain point for us.
Writing
doSomething({
/* large object of options */
})
often feels much less readable than
const options = {
/* large object of options */
}
doSomething(options)
Explicitly annotating const options: DoSomethingOptions = {
helps, but it's a bit cumbersome and hard to spot and enforce in code reviews.
This is bit of an offtopic idea and wouldn't solve most of the use cases for exactness described here, but would it be possible to keep an object literal fresh when it's only used once inside the enclosing scope?
@RyanCavanaugh thanks for explaining EPC...is the difference between EPC and exact types discussed in more detail anywhere? Now I feel like I ought to get a better understanding of why EPC allows some cases that exact types don't.
Hi @noppa I think that would be a great idea. I have just stumbled on this when I noticed the difference between assigning directly versus assingning to a variable first - even asked a question on SO that brought me here. The current behavior is surprising, at least to me...
I believe I have the same problem as the example of GraphQL mutations (exact nested typing, no extra properties should be allowed). In my case, I am thinking of typing API responses in a common module (shared between frontend and backend):
export type ProductsSlashResponse = {
products: Array<{
id: number;
description: string;
}>,
total: number;
};
On the server side, I would like to make sure that the response respects that type signature:
router.get("products/", async () =>
assertType<ProductsSlashResponse>(getProducts())));
I have tried solutions from here. One that seems to work is T extends U ? U extends T ? T : never : never
, along with a curried function which is not ideal. The major problem with it is that you get no feedback on missing or extra properties (perhaps we could improve on that, but it becomes difficult to do when we get into nested properties). Other solutions don't work with deeply nested objects.
Of course, the frontend usually won't crash if I send more information than what is specified, however, this could lead to information leak if the API sends more information than it should (and because of the foggy nature of reading data from a database which types aren't necessarily in sync with the code all the time, this could happen).
@fer22f GraphQL doesn't send fields the client didn't request...unless you're using a JSON scalar type for products
or for the array elements, nothing to worry about there.
Sorry I misread, I thought you meant you were using GraphQL
Someone already mentioned GraphQL, but just in terms of "collecting use cases" (@DanielRosenwasser mentioned several years ago in the thread :-) of "not having any use cases off-hand"), two use cases where I've wanted to use Exact
are:
Passing data into data stores / databases / ORMs--any extra fields that are passed will be silently dropped / not stored.
Passing data into wire calls / RPCs / REST / GraphQL--again any extra fields that are passed will be silently dropped / not sent.
(Well, maybe not silently dropped, they can be runtime errors.)
In both cases I'd like to tell the programmer/myself (via a compile error) "...you really shouldn't give me this extra property, b/c if you're expecting it to get 'stored' or 'sent', it will not be".
This is particularly needed in "partial update" style APIs, i.e. weak types:
type Data = { firstName:? string; lastName?: string; children?: [{ ... }] };
const data = { firstName: "a", lastNmeTypo: "b" };
await saveDataToDbOrWireCall(data);
Passes the weak type check b/c at least one param matched, firstName
, so it's not 100% disjoint, however there is still an "obvious" typo of lsatNmeTypo
that is not getting caught.
Granted, EPC works if I do:
await saveDataToDbOrWireCall({ firstName, lastNmeTypo });
But having to destructure + re-type every field is pretty tedious.
Solutions like @jcalz 's Exactify
work on 1st-level property, but the recursive case (i.e. children
is an array and the array elements should be exact) I'm struggling with once it hits "real world" use cases with generics / like Exact<Foo<Bar<T>>
.
It'd be great to have this built-in, and just wanted to note these explicit use cases (basically wire calls with partial/weak types), if that helps with the prioritization / roadmapping.
(FWIW https://github.com/stephenh/joist-ts/pull/35/files has my current attempt at a deep Exact
and also an Exact.test.ts
that is passing trivial cases, but the PR itself has compile errors on the more esoteric usages. Disclaimer I don't really expect anyone to look into this specific PR, but am just providing it as a "here's where Exact
would be useful" + "AFAICT this is hard to do in user-land" data point.)
Hey,
Was wondering what's the thoughts of the TS team regarding exact types records and tuples proposal here? https://github.com/tc39/proposal-record-tuple
Does it make sense to introduce exact types for those new primitives?
@slorber Not TS, but that's orthogonal. That proposal concerns immutability, and concerns are nearly identical between that and libraries like Immutable.js.
I iterated on @stephenh recursive version. I had some trouble correctly handling undefined cases with recursion, I'm opened to cleaner solution. It probably don't works on some edge cases with arrays or complex data structure.
export type Exact<Expected, Actual> = Expected &
Actual & // Needed to infer `Actual`
(null extends Actual
? null extends Expected
? Actual extends null // If only null stop here, because NonNullable<null> = never
? null
: CheckUndefined<Expected, Actual>
: never // Actual can be null but not Expected: forbid the field
: CheckUndefined<Expected, Actual>);
type CheckUndefined<Expected, Actual> = undefined extends Actual
? undefined extends Expected
? Actual extends undefined // If only undefined stop here, because NonNullable<undefined> = never
? undefined
: NonNullableExact<NonNullable<Expected>, NonNullable<Actual>>
: never // Actual can be undefined but not Expected: forbid the field
: NonNullableExact<NonNullable<Expected>, NonNullable<Actual>>;
type NonNullableExact<Expected, Actual> = {
[K in keyof Actual]: K extends keyof Expected
? Actual[K] extends (infer ActualElement)[]
? Expected[K] extends (infer ExpectedElement)[] | undefined | null
? Exact<ExpectedElement, ActualElement>[]
: never // Not both array
: Exact<Expected[K], Actual[K]>
: never; // Forbid extra properties
};
Exact would be very useful to us when returning API responses. Currently, this is what we resolve to:
const response = { companies };
res.json(exact<GetCompaniesResponse, typeof response>(response));
export function exact<S, T>(object: Exact<S, T>) {
return object;
}
Here the Exact
type is what @ArnaudBarre provided above.
Thanks @ArnaudBarre for unblocking me and teaching me some ts.
Riffing on your solution:
export type Exact<Expected, Actual> =
keyof Expected extends keyof Actual
? keyof Actual extends keyof Expected
? Expected extends ExactElements<Expected, Actual>
? Expected
: never
: never
: never;
type ExactElements<Expected, Actual> = {
[K in keyof Actual]: K extends keyof Expected
? Expected[K] extends Actual[K]
? Actual[K] extends Expected[K]
? Expected[K]
: never
: never
: never
};
// should succeed (produce exactly the Expected type)
let s1: Exact< { a: number; b: string }, { a: number; b: string } >;
let s2: Exact< { a?: number; b: string }, { a?: number; b: string } >;
let s3: Exact< { a?: number[]; b: string }, { a?: number[]; b: string } >;
let s4: Exact< string, string >;
let s5: Exact< string[], string[] >;
let s6: Exact< { a?: number[]; b: string }[], { a?: number[]; b: string }[] >;
// should fail (produce never)
let f1: Exact< { a: string; b: string }, { a: number; b: string } >;
let f2: Exact< { a: number; b: string }, { a?: number; b: string } >;
let f3: Exact< { a?: number; b: string }, { a: number; b: string } >;
let f4: Exact< { a: number[]; b: string }, { a: string[]; b: string } >;
let f5: Exact< { a?: number[]; b: string }, { a: number[]; b: string } >;
let f6: Exact< { a?: number; b: string; c: string }, { a?: number; b: string } >;
let f7: Exact< { a?: number; b: string }, { a?: number; b: string; c: string } >;
let f8: Exact< { a?: number; b: string; c?: string }, { a?: number; b: string } >;
let f9: Exact< { a?: number; b: string }, { a?: number; b: string; c?: string } >;
let f10: Exact< never, string >;
let f11: Exact< string, never >;
let f12: Exact< string, number >;
let f13: Exact< string[], string >;
let f14: Exact< string, string[] >;
let f15: Exact< string[], number[] >;
let f16: Exact< { a?: number[]; b: string }[], { a?: number[]; b: string } >;
The previous solution 'succeeded' for f6, f8 and f9.
This solution also returns 'cleaner' results; when it matches, you get the 'Expected' type back.
As with @ArnaudBarre 's comment ... not sure if all the edge cases are handled, so ymmv ...
@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
@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.
So what the best way to make sure that object has exact properties, no less, no more ?
@captain-yossarian convince TypeScript team to implement this. No solution presented here works for all expected cases, and almost all of them lack clarity.
@toriningen can't imagine how many issues will be closed if TS team will implement this feature
@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:
Those immediate goals serve these ends:
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.
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.
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.
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:
implements
Obviously, there will be cases when you cannot assign literals, but these should also be of a finite set:
as DesiredType
implements
would be nice but otherwise we're goodIn summary, I'm confident that with implements
and fixing EPC (if and when issues arise), exact types should really be handled.
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?
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})
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
@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.
@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" .
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.
Most helpful comment
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)
? :confused: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.