Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Exact types - [WIP] #28749

Closed
wants to merge 10 commits into from
Closed

Conversation

jack-williams
Copy link
Collaborator

@jack-williams jack-williams commented Nov 29, 2018

An initial attempt at Exact Types (#12936). This is not meant be a candidate for merging, but a chance to play around and experiment. The implementation is most likely buggy (re: parser + semicolons), and the errors could be better. Please do experiment with it though!

Semantics:

I write |T| to denote an exact version of T.

  • An object literal O is related to an exact type |T| if all properties in O are in T and O is related to T.
  • An exact type |T| is related to exact object type |U| if all properties in T are in U and T is related to U. (Normal typing handles the converse property check of U in T, I think).
  • An exact type |T| is related to an exact generic mapped type |U| if T is a generic mapped type and T and U have identical constraints.
  • Question: I don't think an exact but non-generic mapped type can be related to an exact generic mapped type. You never know the lower bound on the properties in the generic constraint, so you can never safely say that no properties will be lost by assigning to the exact generic mapped type. Is this right?
  • Excess property checking. There is no excess property checking for exact types as this is (partly) replaced by exactness. Excess property checking is disabled for non-exact types in an exact context. An exact context is within a branch of an union or intersection with at least on exact type. For example.
const contextTest: { | x: unknown; | } & { x: { a: string } } = { x: { a: "foo", b: "bar" } };  // ok

Property b is not reported as excess because the object literal { x: { a: string } } appears in an exact context. The choice came about as a hack. Freshness is turned off for EPC in nested intersection and union, but freshness in needed for exactness. To avoid duplicating essentially the same object flag I chose to keep freshness but turn off EPC in certain cases. I'm not sure if this is the best thing in general, but there are some nice things about it.

Examples:

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

interface A {
    field: string;
}

interface B {
    field2: string;
    field3?: string;
}

type AorB = Exact<A> | Exact<B>;

const fixture: AorB[] = [
    {
        field: 'sfasdf', // error
        field3: 'asd'
    },
];

function foo<T>(x: Exact<T>): T {
    return x;
}

function foobar<T>(x: Exact<T>): Exact<T> {
    return { ...x };
}

foo({x: 1, y: 2}); // ok
foobar({x: 1, y: 2}); // ok

const dict: Record<string, number> = {};

foo(dict); // not ok
foobar(dict); // not ok

type ExactUnion = {| x: {z: string}; |} | {| y: boolean; |};
const a: ExactUnion = { y: true }; // ok
const b: ExactUnion = { x: {z: "hello"} }; // ok
const c: ExactUnion = { x: {z: "hello"}, y: false }; // not ok
const d: ExactUnion = { x: {z: "hello", other: 'hello'} }; // ok -- no excess checks in objects within exact (unless exact)

// to fix use deep exact
type DeepExactUnion = {| x: {| z: string; |}; |} | {| y: boolean; |};
const e: DeepExactUnion = { x: {z: "hello", other: 'hello'} }; // not ok

delete a.y; // error, can't delete on exact type

// Exact Constraints
type AllowedFields = "x" | "y";
type CorrectObject = {[field in AllowedFields]?: number | string};
type HasX = { x: number };

function exactTypes() {
  const checkType = <T>() => <U extends Exact<T>>(value: U) => value;
    const o = checkType<CorrectObject>()({
        x: 1,
        y: "y",
        z: "z", // error, not ok!
    }); // o has type Exact<CorrectObject>

  type HasX = { x: number };
  const oAsHasX: HasX = o; // succeeds
}

const x: {| x: number; y: boolean; |} = {x: 4, y: true};
const y: {| x: number; |} = x; // error

const x1: Exact<{ x: number; y: boolean;}> = {x: 4, y: true};
const y1: {| x: number; |} = x1; // error

const x2: {| x: number; y: boolean; |} = {x: 4, y: true};
const y2: Exact<{x: number; }> = x2; // error

@jack-williams
Copy link
Collaborator Author

For anyone watching: I'm waiting on some fixes for mapped type relations before picking this up again. Main thing to add is decent error messages.

@DanielRosenwasser DanielRosenwasser added the Experiment A fork with an experimental idea which might not make it into master label Dec 14, 2018
@goodmind
Copy link

@jack-williams are you still working on this?

@jack-williams
Copy link
Collaborator Author

jack-williams commented Aug 23, 2019

@goodmind

No. Please feel free to take my changes and start your own experiments if you'd like.

For context: The reason why I stopped persuing this is because the method of object composition in TypeScript (intersection) is essentially meaningless for exact types. I think the correct solution is row types + row polymorphism, but that is a significantly bigger challenge.

@goodmind
Copy link

goodmind commented Aug 23, 2019

@jack-williams that's why Flow has type object spread for exact objects instead of intersection

@goodmind
Copy link

@jack-williams I'm not really familiar with TS codebase, is there some docs?

@jack-williams
Copy link
Collaborator Author

The spread type has been discussed here: #10727. The problem is that you end up with lots of similar concepts that are subtly different and it's not clear when to use what.

I don't think there are any good docs for the compiler itself, or if there are, I don't know where they are located. Sorry I can't be more helpful in this regard.

@sandersn
Copy link
Member

This experiment is pretty old, so I'm going to close it to reduce the number of open PRs.

@sandersn sandersn closed this May 24, 2022
@typescript-bot
Copy link
Collaborator

This PR doesn't have any linked issues. Please open an issue that references this PR. From there we can discuss and prioritise.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Experiment A fork with an experimental idea which might not make it into master
Projects
None yet
Development

Successfully merging this pull request may close these issues.

5 participants