microsoft / TypeScript

TypeScript is a superset of JavaScript that compiles to clean JavaScript output.
https://www.typescriptlang.org
Apache License 2.0
101.28k stars 12.52k forks source link

Exact Types #12936

Open blakeembrey opened 7 years ago

blakeembrey commented 7 years ago

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.

HerringtonDarkholme commented 7 years ago

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

class B {}

type A = | number | 
B

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

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

normalser commented 7 years ago

Not sure if realted but FYI https://github.com/Microsoft/TypeScript/issues/7481

DanielRosenwasser commented 7 years ago

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

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

and then you could write Exact<User>.

joshaber commented 7 years ago

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

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

blakeembrey commented 7 years ago

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

@DanielRosenwasser That looks a lot more reasonable, thanks!

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

rotemdan commented 7 years ago

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

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

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

And without the careful spacing?

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

(answer: Type3, Type5)

blakeembrey commented 7 years ago

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

rotemdan commented 7 years ago

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

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

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

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

That was the best syntax I could find back then.

Edit: just might also work?

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

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

rotemdan commented 7 years ago

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

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

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

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

ethanresnick commented 7 years ago

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

blakeembrey commented 7 years ago

@ethanresnick Why do you believe that?

johnnyreilly commented 7 years ago

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

(Perhaps other errors but not this particular error 😉)

mohsen1 commented 7 years ago

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

exact interface Foo {}
blakeembrey commented 7 years ago

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

mohsen1 commented 7 years ago

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

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

interface Foo {}
type Bar = exact Foo
basarat commented 7 years ago

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:

blakeembrey commented 7 years ago

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

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

RyanCavanaugh commented 7 years ago

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) ? :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.

Jessidhia commented 7 years ago

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

mohsen1 commented 7 years ago

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

ghost commented 7 years ago

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?

Jessidhia commented 7 years ago

Or U is a non-exact subset of T

nerumo commented 7 years ago

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.

leonadler commented 7 years ago

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
blakeembrey commented 7 years ago

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

piotrwitek commented 7 years ago

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

dead-claudia commented 7 years ago

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

nerumo commented 7 years ago

@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

asmundg commented 7 years ago

@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
piotrwitek commented 7 years ago

@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%3B%20%2F%2F%20Error%3A%20Supplied%20parameters%20do%20not%20match%20any%20signature%20of%20call%20target.%0D%0Astore.dispatch(actionCreators.changeBaseCurrency(undefined))%3B%20%2F%2F%20Argument%20of%20type%20'undefined'%20is%20not%20assignable%20to%20parameter%20of%20type%20'string'.%0D%0Astore.dispatch(actionCreators.changeBaseCurrency('USD'))%3B%20%2F%2F%20OK%20%3D%3E%20%7B%20type%3A%20%22CHANGE_BASE_CURRENCY%22%2C%20payload%3A%20'USD'%20%7D) - 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

gcnew commented 7 years ago

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.

magnushiie commented 7 years ago

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 = () => (value: U) => value; const o = checkType()({ 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:
```ts
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
}
magnushiie commented 7 years ago

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

ghost commented 7 years ago

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> = { ... }?

magnushiie commented 7 years ago

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.

magnushiie commented 7 years ago

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.

magnushiie commented 7 years ago

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.

magnushiie commented 7 years ago

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

ghost commented 7 years ago

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

tinganho commented 7 years ago

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
niieani commented 7 years ago

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](https://www.typescriptlang.org/play/#src=%2F%2F%20type%20we'll%20be%20asserting%20as%20exact%3A%0D%0Ainterface%20TextOptions%20%7B%0D%0A%20%20alignment%3A%20string%3B%0D%0A%20%20color%3F%3A%20string%3B%0D%0A%20%20padding%3F%3A%20number%3B%0D%0A%7D%0D%0A%0D%0A%2F%2F%20when%20used%20as%20a%20return%20type%3A%0D%0Afunction%20getDefaultOptions()%3A%20ExactReturn%3Ctypeof%20returnValue%2C%20TextOptions%3E%20%7B%0D%0A%20%20const%20returnValue%20%3D%20%7B%20colour%3A%20'blue'%2C%20alignment%3A%20'right'%2C%20padding%3A%201%20%7D%3B%0D%0A%20%20return%20returnValue%0D%0A%7D%0D%0A%0D%0A%2F%2F%20when%20used%20as%20a%20type%3A%0D%0Afunction%20example(a%3A%20TextOptions)%20%7B%7D%0D%0Aconst%20someInput%20%3D%20%7Bpadding%3A%202%2C%20colour%3A%20''%2C%20alignment%3A%20'right'%7D%0D%0Aexample(someInput%20as%20Exact%3Ctypeof%20someInput%2C%20TextOptions%3E)%0D%0A%0D%0A%0D%0A%2F%2F%20util%20types%3A%0D%0Atype%20Exact%3CA%2C%20B%20extends%20Difference%3CA%2C%20B%3E%3E%20%3D%20AssertPassThrough%3CDifference%3CA%2C%20B%3E%2C%20A%2C%20B%3E%0D%0Atype%20ExactReturn%3CA%2C%20B%20extends%20Difference%3CA%2C%20B%3E%3E%20%3D%20B%20%26%20Exact%3CA%2C%20B%3E%0D%0A%0D%0Atype%20AssertPassThrough%3CActual%2C%20Passthrough%2C%20Expected%20extends%20Actual%3E%20%3D%20Passthrough%3B%0D%0Atype%20Difference%3CA%2C%20Without%3E%20%3D%20%7B%0D%0A%20%20%5BP%20in%20DiffUnion%3Ckeyof%20A%2C%20keyof%20Without%3E%5D%3A%20A%5BP%5D%0D%0A%7D%0D%0Atype%20DiffUnion%3CT%20extends%20string%2C%20U%20extends%20string%3E%20%3D%0D%0A%20%20(%7B%5BP%20in%20T%5D%3A%20P%20%7D%20%26%0D%0A%20%20%7B%20%5BP%20in%20U%5D%3A%20never%20%7D%20%26%0D%0A%20%20%7B%20%5Bk%3A%20string%5D%3A%20never%20%7D)%5BT%5D%3B).

KiaraGrouwstra commented 7 years ago

Nice one! @gcanti (typelevel-ts) and @pelotom (type-zoo) might be interested as well. :)

oleg-codaio commented 6 years ago

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}).

niieani commented 6 years ago

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~~:

jcalz commented 6 years ago

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?

jezzgoodwin commented 6 years ago

Looks like progress is being made on function parameters. Has anyone found a way to enforce exact types for a function return value?

RyanCavanaugh commented 6 years ago

@jezzgoodwin not really. See #241 which is the root cause of function returns not being properly checked for extra properties

michalstocki commented 6 years ago

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.

jcalz commented 6 years ago

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

mohsen1 commented 6 years ago