microsoft / TypeScript

TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

Home Page:https://www.typescriptlang.org

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

Exact Types

blakeembrey opened this issue · comments

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

interface User {
  username: string
  email: string
}

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

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

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

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

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

type Bar = Exact<{ username: string }>

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

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

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

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 #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: #4183 #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 🌹

@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.

Excess Property Checking

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

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

All-optional types

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

Whose type is exact?

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

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

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

Violations of Assumptions / Instantiation Problems

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

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

Miscellany

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

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

Summary: Use Cases Needed

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

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 #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 would be even better

@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.

Proposal

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.

A word on Violations of Assumptions

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:

  1. No excess properties with respect to CorrectObject
  2. Assignable to HasX without specifying HasX as the object's type
type 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?!
    },
];

(Playground)

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 #14094 (comment) 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 #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:

  • Object literal {}, passes. Expected behavior.
  • Numeric constant 1 does not pass. Expected behavior (Argument of type '1' is not assignable to parameter of type 'MyType'.)
  • String literal '' does not pass. Expected behavior (`Argument of type '""' is not assignable to parameter of type 'MyType'.)
  • Arrow function declaration () => {}: Passes. Not expected behavior. Probably passes because functions are objects?
  • Class method console.log Passes. Not expected behavior. Similar to arrow function.
  • Class 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:

  • Object literals pass as expected, as they already do with {[key: string]: any}.
  • Numeric constants fail as expected, since literals aren't assignable to {[key: string]: any}.
  • String literals fail as expected, since they aren't assignable to {[key: string]: any}.
  • Functions and class constructors fail because of their call signature (it's not a string property).
  • The 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 interfaces to be open. In contrast, I expect types 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;
}

Sample usage:

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:

#1 You can set the return type to be the same as the input type with 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
   };
}

#2 For reducers, instead of a temporary variable, assign it back to itself before returning:

interface State {
   name: string;
}
function nameReducer(state: State, action: Action<string>) {
   return (state = {
       ...state,
       fullName: action.payload        // THIS IS REPORTED AS AN ERROR
   });
}

#3 If you really want a temporary variable, don't give it an explicit type, use typeof state again

interface 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;
}

#3b If your reducer doesn't contain ...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):

  • Avoid typos of property names
  • If we add a property to some 'component', we want to make sure everything that uses it has to add that property too
  • If we remove a property from some 'component' we need to remove it everywhere.
  • Make sure we don't provide extra properties unnecessarily (maybe we're sending it to an API and we want to keep payload lean)

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.

  • "Nothing less than" is just normal types. TS does this implicitly, and every type is treated as equivalent to a for all T extends X: T.
  • "Nothing more than" is basically the opposite: it's an implicit 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.
  • Since this just toggles assignability, you could still have things like unions that are exact or super types.

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:

Strong typing for 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?

Hardening optional property widening.

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.

React and HOC

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

See

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: #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?

commented

@Kotarski Did you publish it by any chance in NPM registry?

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.

@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.