microsoft / TypeScript

TypeScript is a superset of JavaScript that compiles to clean JavaScript output.
https://www.typescriptlang.org
Apache License 2.0
101.31k stars 12.53k 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.

dead-claudia commented 5 years ago

@RyanCavanaugh Okay, so let me clarify: I intuitively read T extends U ? F<T> : G<T> as T <: U ⊢ F(T), (T <: U ⊢ ⊥) ⊢ G(T), with the comparison done not piecewise, but as a complete step. That's distinctly different from "the union of for all {if t ∈ U then F({t}) else G({t}) | t ∈ T}, which is what's currently the semantics.

(Pardon if my syntax is a bit off - my type theory knowledge is entirely self-taught, so I know I don't know all the syntactic formalisms.)

RyanCavanaugh commented 5 years ago

Which operation is more intuitive is up for infinite debate, but with the current rules it's easy to make a distributive type non-distributive with [T] extends [C]. If the default were non-distributive, you'd need some new incantation at a different level to cause distributivity. That's also a separate question from which behavior is more often preferred; IME I almost never want a non-distributing type.

jack-williams commented 5 years ago

Ye there is no strong theoretical grounding for distribution because it’s a syntactic operation.

The reality is that it is very useful and trying to encode it some other way would be painful.

dead-claudia commented 5 years ago

As it stands, I'll go ahead and trail off before I drive the conversation too far off topic.

zpdDG4gta8XKpMCd commented 5 years ago

there are so many issues about distrubutivness already, why won't we face it that new syntax is required?

30572

spion commented 5 years ago

Here is an example problem:

I want to specify that my users API endpoint/serice must NOT return any extra properties (like e.g. password) other than the ones specified in the service interface. If I accidentally return an object with extra properties, I want a compile time error, regardless of whether the result object has been produced by an object literal or otherwise.

A run time check of every returned object can be costly, especially for arrays.

Excess property checking doesn't help in this case. Honestly, I think its a wonky one-trick-pony solution. In theory it should've provided an "it just works" kind of experience - in practice its also a source of confusion Exact object types should have been implemented instead, they would have covered both use cases nicely.

jack-williams commented 5 years ago

@babakness Your type NoExcessiveProps is a no-op. I think they mean something like this:

interface API {
    username: () => { username: string }
}

const api: API = {
    username: (): { username: string } => {
        return { username: 'foobar', password: 'secret'} // error, ok
    }
}

const api2: API = {
    username: (): { username: string } => {
        const id: <X>(x: X) => X = x => x;
        const value = id({ username: 'foobar', password: 'secret' });
        return value  // no error, bad?
    }
}

As the writer of the API type you want to enforce that username just returns the username, but any implementer can get around that because object types have no width restriction. That can only be applied at the initialisation of a literal, which the implementer may, or may not, do. Though, I would heavily discourage anyone from trying to use exact types as language based security.

@spion

Excess property checking doesn't help in this case. Honestly, I think its a wonky one-trick-pony solution. In theory they should've provided an "it just works" kind of experience

EPC is a reasonably sensible and lightweight design choice that covers are large set of problem. The reality is that Exact types do not 'just work'. To implement in a sound way that supports extensibility requires a completely different type system.

spion commented 5 years ago

@jack-williams Of course there would be other ways to verify present as well (runtime checks where performance is not an issue, tests etc) but an additional compile-time one is invaluable for fast feedback.

Also, I didn't mean that exact types "just work". I meant that EPC was meant to "just work" but in practice its just limited, confusing and unsafe. Mainly because if try to "deliberately" use it you generally end up shooting yourself in the foot.

edit: Yep, I edited to replace "they" with "it" as I realized its confusing.

jack-williams commented 5 years ago

@spion

Also, I didn't mean that exact types "just work". I meant that EPC was meant to "just work" but in practice its just limited, confusing and unsafe. Mainly because if try to "deliberately" use it you generally end up shooting yourself in the foot.

My mistake. Read the original comment as

In theory they should've provided an "it just works" kind of experience [which would have been exact types instead of EPC]

commentary in [] being my reading.

The revised statement:

In theory it should've provided an "it just works" kind of experience

is much clearer. Sorry for my misinterpretation!

babakness commented 5 years ago
type NoExcessiveProps<O> = {
  [K in keyof O]: K extends keyof O ? O[K] : never 
}

// no error
const getUser1 = (): {username: string} => {
  const foo = {username: 'foo', password: 'bar' }
  return foo 
} 

// Compile-time error, OK
const foo: NoExcessiveProps<{username: string}>  = {username: 'a', password: 'b' }

// No error? 🤔
const getUser2 = (): NoExcessiveProps<{username: string}> => {
  const foo = {username: 'foo', password: 'bar' }
  return foo 
}

The result for getUser2 is surprising, it feels inconsistent and like it should produce a compile-time error. Whats the insight on why it doesn't?

dragomirtitian commented 5 years ago

@babakness Your NoExcessiveProps just evaluates back to T (well a type with the same keys as T). In [K in keyof O]: K extends keyof O ? O[K] : never, K will always be a key of O since you are mapping over keyof O. Your const example errors because it triggers EPC just as it would have if you would have typed it as {username: string}.

If you don't mind calling an extra function we can capture the actual type of the object passed in, and do a custom form of excess property checks. (I do realize the whole point is to automatically catch this type of error, so this might be of limited value):


function checked<T extends E, E>(o: T & Record<Exclude<keyof T, keyof E>, never>): E {
    return o;
}

const getUser2 = (): { username: string } => {
    const foo = { username: 'foo', password: 'bar' }
    return checked(foo) //error
}
const getUser3 = (): { username: string } => {
    const foo = { username: 'foo' }
    return checked(foo) //ok
}
babakness commented 5 years ago

@dragomirtitian Ah... right... good point! So I'm trying to understand your checked function. I'm particularly puzzled

const getUser2 = (): { username: string } => {
    const foo = { username: 'foo', password: 'bar' }
    const bar = checked(foo) // error
    return checked(foo) //error
}
const getUser3 = (): { username: string } => {
    const foo = { username: 'foo' }
    const bar = checked(foo) // error!?
    return checked(foo) //ok
}

The bar assignment in getUser3 fails. The error seems to be at foo

image

Details of the error

image

The type for bar here is {}, which seems as though it is because on checked

function checked<T extends E, E>(o: T & Record<Exclude<keyof T, keyof E>, never>): E {
    return o;
}

E is not assigned anywhere. Yet if we replace typeof E with typeof {}, it doesn't work.

What is the type for E? Is there some kind of context-aware thing happening?

dragomirtitian commented 5 years ago

@babakness If there is no other place to infer a type parameter from, typescript will infer it from the return type. So when we are assigning the result of checked to the return of getUser*, E will be the return type of the function, and T will be the actual type of the value you want to return. If there is no place to infer E from it will just default to {} and so you will always get an error.

The reason I did it like this was to avoid any explicit type parameters, you could create a more explicit version of it:

function checked<E>() {
    return function <T extends E>(o: T & Record<Exclude<keyof T, keyof E>, never>): E {
        return o;
    }
}

const getUser2 = (): { username: string } => {
    const foo = { username: 'foo', password: 'bar' }
    return checked<{ username: string }>()(foo) //error
}
const getUser3 = (): { username: string } => {
    const foo = { username: 'foo' }
    return checked<{ username: string }>()(foo) //ok
}

Note: The curried function approach is necessary since we don't yet have partial argument inference (https://github.com/Microsoft/TypeScript/pull/26349) so we can't specify some type parameter and have others inferred in the same call. To get around this we specify E in the first call and let T be inferred in the second call. You could also cache the cache function for a specific type and use the cached version

function checked<E>() {
    return function <T extends E>(o: T & Record<Exclude<keyof T, keyof E>, never>): E {
        return o;
    }
}
const checkUser = checked<{ username: string }>()

const getUser2 = (): { username: string } => {
    const foo = { username: 'foo', password: 'bar' }
    return checkUser(foo) //error
}
const getUser3 = (): { username: string } => {
    const foo = { username: 'foo' }
    return checkUser(foo) //ok
}
spion commented 5 years ago

FWIW this is a WIP / sketch tslint rule that solves the specific problem of not accidentally returning extra properties from "exposed" methods.

https://gist.github.com/spion/b89d1d2958f3d3142b2fe64fea5e4c32

borekb commented 5 years ago

For the spread use case – see https://github.com/Microsoft/TypeScript/issues/12936#issuecomment-300382189 – could a linter detect a pattern like this and warn that it's not type-safe?

Copying code example from the aforementioned comment:

interface State {
   name: string;
}
function nameReducer(state: State, action: Action<string>): State {
   return {
       ...state,
       fullName: action.payload // compiles, but it's an programming mistake
   }
}

cc @JamesHenry / @armano2

helios1138 commented 5 years ago

Would very much like to see that happen. We use generated TypeScript definitions for GraphQL endpoints and it's a problem that TypeScript does not raise an error when I pass an object with more fields than necessary to a query because GraphQL will fail to execute such a query at runtime.

danielnmsft commented 5 years ago

how much of this is now addressed with the 3.5.1 update w/ better checking for extra properties during assignment? we got a bunch of known problem areas flagged as errors the way we wanted them to be after upgrading to 3.5.1

OliverJAsh commented 5 years ago

if you have a problem and you think exact types are the right solution, please describe the original problem here

https://github.com/microsoft/TypeScript/issues/12936#issuecomment-284590083

Here's one involving React refs: https://github.com/microsoft/TypeScript/issues/31798

/cc @RyanCavanaugh

jeremybparagon commented 5 years ago

One use case for me is

export const mapValues =
  <T extends Exact<T>, V>(object: T, mapper: (value: T[keyof T], key: keyof T) => V) => {
    type TResult = Exact<{ [K in keyof T]: V }>;
    const result: Partial<TResult> = { };
    for (const [key, value] of Object.entries(object)) {
      result[key] = mapper(value, key);
    }
    return result as TResult;
  };

This is unsound if we don't use exact types, since if object has extra properties, it's not safe to call mapper on those extra keys and values.

The real motivation here is that I want to have the values for an enum somewhere that I can reuse in the code:

const choices = { choice0: true, choice1: true, choice2: true };
const callbacksForChoices = mapValues(choices, (_, choice) => () => this.props.callback(choice));

where this.props.callback has type (keyof typeof choices) => void.

jeremybparagon commented 5 years ago

So really it's about the type system being able to represent the fact that I have a list of keys in code land that exactly matches a set (e.g., a union) of keys in type land, so that we can write functions that operate on this list of keys and make valid type assertions about the result. We can't use an object (choices in my previous example) because as far as the type system knows, the code-land object could have extra properties beyond whatever object type is used. We can't use an array (['choice0', 'choice1', 'choice2'] as const, because as far as the type system knows, the array might not contain all of the keys allowed by the array type.

phaux commented 5 years ago

Maybe exact shouldn't be a type, but only a modifier on function's inputs and/or output? Something like flow's variance modifier (+/-)

donabrams commented 5 years ago

I want to add on to what @phaux just said. The real use I have for Exact is to have the compiler guarantee the shape of functions. When I have a framework, I may want either of these: (T, S): AtMost<T>, (T, S): AtLeast<T>, or (T, S): Exact<T> where the compiler can verify that the functions a user defines will fit exactly.

Some useful examples: AtMost is useful for config (so we don't ignore extra params/typos and fail early). AtLeast is great for things like react components and middleware where a user may shove whatever extra they want onto an object. Exact is useful for serialisation/deserialization (we can guarantee we don't drop data and these are isomorphic).

lostpebble commented 5 years ago

Would this help to prevent this from happening?

interface IDate {
  year: number;
  month: number;
  day: number;
}

type TBasicField = string | number | boolean | IDate;

 // how to make this generic stricter?
function doThingWithOnlyCorrectValues<T extends TBasicField>(basic: T): void {
  // ... do things with basic field of only the exactly correct structures
}

const notADate = {
  year: 2019,
  month: 8,
  day: 30,
  name: "James",
};

doThingWithOnlyCorrectValues(notADate); // <- this should not work! I want stricter type checking

We really need a way in TS to say T extends exactly { something: boolean; } ? xxx : yyy.

Or otherwise, something like:

const notExact = {
  something: true,
  name: "fred",
};

Will still return xxx there.

pleerock commented 5 years ago

Maybe const keyword can be used? e.g.T extends const { something: boolean }

lostpebble commented 5 years ago

@pleerock it might be slightly ambiguous, as in JavaScript / TypeScript we can define a variable as const but still add / remove object properties. I think the keyword exact is pretty to the point.

mityok commented 5 years ago

I'm not sure if it's exactly related, but i'd expect at least two errors in this case: playground Screen Shot 2019-08-08 at 10 15 34

lostpebble commented 5 years ago

@mityok I think that is related. I'm guessing you would like to do something along the lines of:

class Animal {
  makeSound(): exact Foo {
     return { a: 5 };
  }
}

If the exact made the type stricter - then it shouldn't be extendable with an extra property, as you've done in Dog.

pocesar commented 5 years ago

taking advantage of the const (as const) and using before interfaces and types, like

const type WillAcceptThisOnly = number

function f(accept: WillAcceptThisOnly) {
}

f(1) // error
f(1 as WillAcceptThisOnly) // ok, explicit typecast

const n: WillAcceptThisOnly = 1
f(n) // ok

would be really verbose having to assign to const variables, but would avoid a lot of edge cases when you pass a typealias that wasn't exact what you were expecting

toriningen commented 5 years ago

I have came up with pure TypeScript solution for Exact<T> problem that, I believe, behaves exactly like what has been requested in the main post:

// (these two types MUST NOT be merged into a single declaration)
type ExactInner<T> = <D>() => (D extends T ? D : D);
type Exact<T> = ExactInner<T> & T;

function exact<T>(obj: Exact<T> | T): Exact<T> {
    return obj as Exact<T>;
};

The reason ExactInner must be not included in the Exact is due to #32824 fix not being released yet (but already merged in !32924).

It's only possible to assign a value to the variable or function argument of type Exact<T>, if the right hand expression is also Exact<T>, where T is exactly identical type in both parts of assignment.

I haven't achieved automatic promotion of values into Exact types, so that's what exact() helper function is for. Any value can be promoted to be of exact type, but assignment will only succeed if TypeScript can prove that underlying types of both parts of expression are not just extensible, but exactly the same.

It works by exploiting the fact that TypeScript uses extend relation check to determine if right hand type can be assigned to the left hand type — it only can if right hand type (source) extends the left hand type (destination).

Quoting checker.ts,

// Two conditional types 'T1 extends U1 ? X1 : Y1' and 'T2 extends U2 ? X2 : Y2' are related if // one of T1 and T2 is related to the other, U1 and U2 are identical types, X1 is related to X2, // and Y1 is related to Y2.

ExactInner<T> generic uses the described approach, substituting U1 and U2 with underlying types that require exactness checks. Exact<T> adds an intersection with plain underlying type, which allows TypeScript to relax exact type when its target variable or function argument are not an exact type.

From programmer's perspective, Exact<T> behaves as if it sets an exact flag on T, without inspecting T or changing it, and without creating an independent type.

Here are playground link and gist link.

Possible future improvement would be to allow auto-promotion of non-exact types into exact types, completely removing the need in exact() function.

codeandcats commented 5 years ago

Amazing work @toriningen!

If anyone is able to find a way to make this work without having to wrap your value in a call to exact it would be perfect.

lookfirst commented 5 years ago

Not sure if this is the right issue, but here is an example of something I'd like to work.

https://www.typescriptlang.org/play/#code/KYOwrgtgBAyg9gJwC4BECWDgGMlriKAbwCgooBBAZyygF4oByAQ2oYBpSoVhq7GATHlgbEAvsWIAzMCBx4CTfvwDyCQQgBCATwAU-DNlz4AXFABE5GAGEzUAD7mUAUWtmAlEQnjiilWuCauvDI6Jhy+AB0VFgRSHAAqgAOiQFWLMA6bm4A3EA

enum SortDirection {
  Asc = 'asc',
  Desc = 'desc'
}
function addOrderBy(direction: "ASC" | "DESC") {}
addOrderBy(SortDirection.Asc.toUpperCase());
dead-claudia commented 5 years ago

@lookfirst That's different. This is asking for a feature for types that don't admit extra properties, like some type exact {foo: number} where {foo: 1, bar: 2} isn't assignable to it. That's just asking for text transforms to apply to enum values, which likely doesn't exist.

Not sure if this is the right issue, but [...]

In my experience as a maintainer elsewhere, if you're in doubt and couldn't find any clear existing issue, file a new bug and worst case scenario, it gets closed as a dupe you didn't find. This is pretty much the case in most major open source JS projects. (Most of us bigger maintainers in the JS community are actually decent people, just people who can get really bogged down over bug reports and such and so it's hard not to be really terse at times.)

lookfirst commented 5 years ago

@isiahmeadows Thanks for the response. I didn't file a new issue because I was searching for duplicate issues first, which is the correct thing to do. I was trying to avoid bogging people down because I wasn't sure if this was the right issue or not or even how to categorize what I was talking about.

eqyiel commented 5 years ago

Another sample use case:

https://www.typescriptlang.org/play/index.html#code/JYOwLgpgTgZghgYwgAgIoFdoE8AKcpwC2EkUAzsgN4BQydycAXMmWFKAOYDct9ARs1bsQ3agF9q1GOhAIwwAPYhkCKBDiQAKgAtgUACZ4oYXHA4QAqgCUAMgAoAjpiimChMswzYjREtDIAlFS8dGpg6FDKAAbaYGAADh4A9EkQAB5E8QA2EAB0CAqESQD8ACSUIBAA7sjWNgDK6lAI2j7udgDyfABWEHK5EODsEGSOzq5EgQFiUeKSKcgAolBQCuQANAwU6fF9kPrUqupaugZGJnjmdXaUDMwA5PebAsiPmwgPYLpkALTx+EQflVgFksj8EHB0GQID8vnp9H98CZEeZYQpwQQyNp7sgxAEeIclKxkP83BQALxUO6vJ7IF5vFSfb6ItxAkFgiFQmFwgws5H-VFgdGqOBYnFiAkLQC8G4BGPeQJl2Km0fQA1pxkPoIDBjhB9HSsFsyMAOCB1cAwPKFAwSQDiKRDmoNBAdPDzqYrrY7KTJvigA

iamandrewluca commented 5 years ago

EDITED: Check @aigoncharov solution bellow, because I think is even faster.

type Exact<T, R> = T extends R
  ? R extends T
    ? T
    : never
  : never

Don't know if this can be improved more.

type Exact<T, Shape> =
    // Check if `T` is matching `Shape`
    T extends Shape
        // Does match
        // Check if `T` has same keys as `Shape`
        ? Exclude<keyof T, keyof Shape> extends never
            // `T` has same keys as `Shape`
            ? T
            // `T` has more keys than `Shape`
            : never
        // Does not match at all
        : never;

type InexactType = {
    foo: string
}

const obj = {
    foo: 'foo',
    bar: 'bar'
}

function test1<T>(t: Exact<T, InexactType>) {}
function test2(t: InexactType) {}

test1(obj) // $ExpectError
test2(obj)

Without comments

type ExactKeys<T1, T2> = Exclude<keyof T1, keyof T2> extends never
    ? T1
    : never

type Exact<T, Shape> = T extends Shape
    ? ExactKeys<T, Shape>
    : never;
aigoncharov commented 5 years ago

Don't know if this can be improved more.

type Exact<T, Shape> =
    // Check if `T` is matching `Shape`
    T extends Shape
        // Does match
        // Check if `T` has same keys as `Shape`
        ? Exclude<keyof T, keyof Shape> extends never
            // `T` has same keys as `Shape`
            ? T
            // `T` has more keys than `Shape`
            : never
        // Does not match at all
        : never;

type InexactType = {
    foo: string
}

const obj = {
    foo: 'foo',
    bar: 'bar'
}

function test1<T>(t: Exact<T, InexactType>) {}
function test2(t: InexactType) {}

test1(obj) // $ExpectError
test2(obj)

Without comments

type ExactKeys<T1, T2> = Exclude<keyof T1, keyof T2> extends never
    ? T1
    : never

type Exact<T, Shape> = T extends Shape
    ? ExactKeys<T, Shape>
    : never;

Love that idea!

Another trick that could do the job is to check assignability in both directions.

type Exact<T, R> = T extends R
  ? R extends T
    ? T
    : never
  : never

type A = {
  prop1: string
}
type B = {
  prop1: string
  prop2: string
}
type C = {
  prop1: string
}

type ShouldBeNever = Exact<A, B>
type ShouldBeA = Exact<A, C>

http://www.typescriptlang.org/play/#code/C4TwDgpgBAogHgQwMbADwBUA0UBKA+KAXinSgjmAgDsATAZ1wCgooB+XMi6+k5lt3vygAuKFQgA3CACc+o8VNmNQkKAEEiUAN58w0gPZgAjKLrBpASyoBzRgF9l4aACFNOlnsMmoZyzd0GYABMpuZWtg4q0ADCbgFeoX4RjI6qAMoAFvoArgA2NM4QAHKSMprwyGhq2M54qdCZOfmFGsQVKKjVUNF1QkA

Another playground from @iamandrewluca https://www.typescriptlang.org/play/?ssl=7&ssc=6&pln=7&pc=17#code/C4TwDgpgBAogHgQwMbADwBUA0UBKA+KAXinSgjmAgDsATAZ1wCgooB+XMi6+k5lt3vygAuKFQgA3CACc+o8VNmNQkKAElxiFOnDRiAbz4sAZgHtTousGkBLKgHNGAX0aMkpqlaimARgCsiKEMhMwsoAHJQ8MwjKB8EaVFw+Olw51djAFcqFBsPKEorAEYMPAAKYFF4ZDQsdU0anUg8AEoglyyc4DyqAogrACYK0Q1yRt02-RdlfuAist8-NoB6ZagAEnhIFBhpaVNZQuAhxZagA

jeremybparagon commented 5 years ago

A nuance here is whether Exact<{ prop1: 'a' }> should be assignable to Exact<{ prop1: string }>. In my use cases, it should.

iamandrewluca commented 5 years ago

@jeremybparagon your case is covered. Here are some more cases.

type InexactType = {
    foo: 'foo'
}

const obj = {
    // here foo is infered as `string`
    // and will error because `string` is not assignable to `"foo"`
    foo: 'foo',
    bar: 'bar'
}

function test1<T>(t: Exact<T, InexactType>) {}
function test2(t: InexactType) {}

test1(obj) // $ExpectError
test2(obj) // $ExpectError
type InexactType = {
    foo: 'foo'
}

const obj = {
    // here we cast to `"foo"` type
    // and will not error
    foo: 'foo' as 'foo',
    bar: 'bar'
}

function test1<T>(t: Exact<T, InexactType>) {}
function test2(t: InexactType) {}

test1(obj) // $ExpectError
test2(obj)
type Exact<T, R> = T extends R
  ? R extends T
    ? T
    : never
  : never
dragomirtitian commented 5 years ago

I think anybody using this trick (and I'm not saying there aren't valid uses for it) should be acutely aware that it is very very easy to get more props in the "exact" type. Since InexactType is assignable to Exact<T, InexactType> if you have something like this, you break out of exactness without realizing it:

function test1<T>(t: Exact<T, InexactType>) {}

function test2(t: InexactType) {
  test1(t); // inexactType assigned to exact type
}
test2(obj) // but 

Playground Link

This is the reason (at least one of them) that TS does not have exact types, as it would require a complete forking of object types in exact vs non-exact types where an inexact type is never assignable to an exact one, even if at face value they are compatible. The inexact type may always contain more properties. (At least this was one of the reasons @ahejlsberg mentioned as tsconf).

If asExact were some syntactic way of marking such an exact object, this is what such a solution might look like:

declare const exactMarker: unique symbol 
type IsExact = { [exactMarker]: undefined }
type Exact<T extends IsExact & R, R> =
  Exclude<keyof T, typeof exactMarker> extends keyof R? T : never;

type InexactType = {
    foo: string
}
function asExact<T>(o: T): T & IsExact { 
  return o as T & IsExact;
}

const obj = asExact({
  foo: 'foo',
});

function test1<T extends IsExact & InexactType>(t: Exact<T, InexactType>) {

}

function test2(t: InexactType) {
  test1(t); // error now
}
test2(obj) 
test1(obj);  // ok 

const obj2 = asExact({
  foo: 'foo',
  bar: ""
});
test1(obj2);

const objOpt = asExact < { foo: string, bar?: string }>({
  foo: 'foo',
  bar: ""
});
test1(objOpt);

Playground Link

toriningen commented 5 years ago

@dragomirtitian that's why I came up with the solution a bit earlier https://github.com/microsoft/TypeScript/issues/12936#issuecomment-524631270 that doesn't suffer from this.

aigoncharov commented 5 years ago

@dragomirtitian it's a matter of how you type your functions. If you do it a little differently, it works.

type Exact<T, R> = T extends R
  ? R extends T
    ? T
    : never
  : never

type InexactType = {
    foo: string
}

const obj = {
    foo: 'foo',
    bar: 'bar'
}

function test1<T>(t: Exact<T, InexactType>) {}

function test2<T extends InexactType>(t: T) {
  test1(t); // fails
}
test2(obj)

https://www.typescriptlang.org/play/#code/C4TwDgpgBAogHgQwMbADwBUA0UBKA+KAXinSgjmAgDsATAZ1wCgooB+XMi6+k5lt3vygAuKFQgA3CACc+o8VNmNQkKAElxiFOnDRiAbz4sAZgHtTousGkBLKgHNGAX0aMkpqlaimARgCsiKEMhMwsoAHJQ8MwjKB8EaVFw+Olw51djAFcqFBsPKEorAEYMPAAKYFF4ZDQsdU0anUg8AEogl0YsnOA8qgKIKwAmDE5KWgYNckbdcsqSNuD+4oqWgG4oAHoNqGMEGwAbOnTC4EGy3z82oA

jeremybparagon commented 5 years ago

@jeremybparagon your case is covered.

@iamandrewluca I think the solutions here and here differ on how they treat my example.

type Exact<T, R> = T extends R
  ? R extends T
    ? T
    : never
  : never

type A = {
  prop1: 'a'
}
type C = {
  prop1: string
}

type ShouldBeA = Exact<A, C> // This evaluates to never.

const ob...

Playground Link

dragomirtitian commented 5 years ago

@aigoncharov The problem is you need to be aware of that so one could easily not do this and test1 could still get called with extra properties. IMO any solution that can so easily allow an accidental inexact assignment has already failed as the whole point is to enforce exactness in the type system.

@toriningen yeah your solution seems better, I was just referring to the last posted solution. Your solution has going for it the fact that you don't need the extra function type parameter, however it does not seem to work well for optional properties:

// (these two types MUST NOT be merged into a single declaration)
type ExactInner<T> = <D>() => (D extends T ? D : D);
type Exact<T> = ExactInner<T> & T;
type Unexact<T> = T extends Exact<infer R> ? R : T;

function exact<T>(obj: Exact<T> | T): Exact<T> {
    return obj as Exact<T>;
};

////////////////////////////////
// Fixtures:
type Wide = { foo: string, bar?: string };
type Narrow = { foo: string };
type ExactWide = Exact<Wide>;
type ExactNarrow = Exact<Narrow>;

const ew: ExactWide = exact<Wide>({ foo: "", bar: ""});
const assign_en_ew: ExactNarrow = ew; // Ok ? 

Playground Link

@jeremybparagon I'm not sure @aigoncharov 's solution does a good job on optional properties though. Any solution based on T extends S and S extends T will suffer from the simple fact that

type A = { prop1: string }
type C = { prop1: string,  prop2?: string }
type CextendsA = C extends A ? "Y" : "N" // Y 
type AextendsC = A extends C ? "Y" : "N" // also Y 

Playground Link

I think @iamandrewluca of using Exclude<keyof T, keyof Shape> extends never is good, my type is quite similar (I edited my original answer to add the &R to ensure T extends R without any extra checks).

type Exact<T extends IsExact & R, R> =
  Exclude<keyof T, typeof exactMarker> extends keyof R? T : never;

I would not stake my reputation that my solution does not have holes though, I haven't looked that hard for them but welcome any such findings 😊

stevemarksd commented 4 years ago

we should have a flag where this is enabled globally. In this way, who wants to loose type can keep doing the same. Way too many bugs caused by this issue. Now I try to try to avoid spread operator and use pickKeysFromObject(shipDataRequest, ['a', 'b','c'])

dallonf commented 4 years ago

Here's a use case for exact types I recently stumbled on:

type PossibleKeys = 'x' | 'y' | 'z';
type ImmutableMap = Readonly<{ [K in PossibleKeys]?: string }>;

const getFriendlyNameForKey = (key: PossibleKeys) => {
    switch (key) {
        case 'x':
            return 'Ecks';
        case 'y':
            return 'Why';
        case 'z':
            return 'Zee';
    }
};

const myMap: ImmutableMap = { x: 'foo', y: 'bar' };

const renderMap = (map: ImmutableMap) =>
    Object.keys(map).map(key => {
        // Argument of type 'string' is not assignable to parameter of type 'PossibleKeys'
        const friendlyName = getFriendlyNameForKey(key);
        // No index signature with a parameter of type 'string' was found on type 'Readonly<{ x?: string | undefined; y?: string | undefined; z?: string | undefined; }>'.    
        return [friendlyName, map[key]];
    });
;

Because types are inexact by default, Object.keys has to return a string[] (see https://github.com/microsoft/TypeScript/pull/12253#issuecomment-263132208), but in this case, if ImmutableMap was exact, there's no reason it couldn't return PossibleKeys[].

RyanCavanaugh commented 4 years ago

@dallonf note that this example requires extra functionality besides just exact types -- Object.keys is just a function and there'd need to be some mechanism for describing a function that returns keyof T for exact types and string for other types. Simply having the option to declare an exact type wouldn't be sufficient.

dead-claudia commented 4 years ago

@RyanCavanaugh I think that was the implication, exact types + the ability to detect them.

eps1lon commented 4 years ago

Use case for the react typings:

forwardRef<T, P>(render: (props: P, ref: Ref<T>) => ReactElement<P> & { displayName?: string }) => ComponentType<P>.

It's tempting to pass a regular component to forwardRef which is why React issues runtime warnings if it detects propTypes or defaultProps on the render argument. We'd like to express this at the type level but have to fallback to never:

- forwardRef<T, P>(render: (props: P, ref: Ref<T>) => ReactElement<P> & { displayName?: string }) => ComponentType<P>
+ forwardRef<T, P>(render: (props: P, ref: Ref<T>) => ReactElement<P> & { displayName?: string, propTypes?: never, defaultProps?: never }) => ComponentType<P>

The error message with never is not helpful ("{} is not assignable to undefined").

sarimarton commented 4 years ago

Can someone help me out on how @toriningen's solution would look like with a union of different event object shapes? I want to restrict my event shapes in redux-dispatch calls, e.g.:

type StoreEvent =
  | { type: 'STORE_LOADING' }
  | { type: 'STORE_LOADED'; data: unknown[] }

It's unclear how I could make a typed dispatch() function which only accepts the exact shape of an event.

(UPDATE: I figured it out: https://gist.github.com/sarimarton/d5d539f8029c01ca1c357aba27139010)

nodkz commented 4 years ago

Use case:

Missing Exact<> support leads to runtime problems with GraphQL mutations. GraphQL accepts exact list of permitted properties. If you provide excessive props, it throws an error.

So when we obtain some data from the form, then Typescript cannot validate excess (extra) properties. And we will get an error at runtime.

The following example illustrates imaginary safety

Try in Playground

Screen Shot 2020-03-05 at 13 04 38

According to the article https://fettblog.eu/typescript-match-the-exact-object-shape/ and similar solutions provided above we can use the following ugly solution:

Screen Shot 2020-03-05 at 12 26 57

Why this savePerson<T>(person: ValidateShape<T, Person>) solution is Ugly?

Assume you have deeply nested input type eg.:

// Assume we are in the ideal world where implemented Exact<>

type Person {
  name: string;
  address: Exact<Address>;
}

type Address {
   city: string
   location: Exact<Location>
}

type Location {
   lon: number;
   lat: number; 
}

savePerson(person: Exact<Person>)

I cannot imagine what spaghetti we should write to get the same behavior with the currently available solution:

savePerson<T, TT, TTT>(person: 
  ValidateShape<T, Person keyof ...🤯...
     ValidateShape<TT, Address keyof ...💩... 
         ValidateShape<TTT, Location keyof ...🤬... 
> > >)

So, for now, we have big holes in static analysis in our code, which works with complex nested input data.