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

Arranged Field Definitions #58019

Closed
RyanCavanaugh opened this issue Apr 1, 2024 · 13 comments
Closed

Arranged Field Definitions #58019

RyanCavanaugh opened this issue Apr 1, 2024 · 13 comments
Assignees
Labels
Experimentation Needed Someone needs to try this out to see what happens

Comments

@RyanCavanaugh
Copy link
Member

RyanCavanaugh commented Apr 1, 2024

Arranged Field Definitions

In TypeScript 5.5.555 (codename "five by five"), we're excited to introduce a new feature we're calling "arranged field definitions", or AFD. Let's look at the feature and how we got here.

Background

Ever since the first prototype of TypeScript, our goal has been to enumerate the ways that different values exist at runtime and to provide a way for programmers, on their own, to describe these key differences. If there are two values you can tell apart at runtime, TypeScript should be able to tell them apart too.

Inspiration

We were trying to find a good string padding library when we ran across this piece of code:

function isGoodObject(obj) {
    return Object.keys(obj).reduce((last, next) => last === null ? last : last < next ? next : null, "") !== null;
}

What was the purpose of this? What genius wrote it? Why use null instead of Symbol.for("null") ? Does this count as point-free style, or just zero points? Those answers are lost to the sands of time. Regardless, the functionality of this snippet wasn't apparent until we started playing with it:

> isGoodObject({ x: 1, y: 2 })
< true

> isGoodObject({ y: 2, x: 1 })
< false

> isGoodObject({ a: "hello", b: "world" })
< true

> isGoodObject({ b: "world", a: "hello" })
< false

It was a brilliant "Ah-ha!" moment for everyone on the team. I think Anders even spilled his coffee. Someone in JS was enforcing the arrangement of property keys in objects - in this case, that they be sorted. This was uncharted territory for us, and we immediately set to work on building a type representation for it.

TypeScript needed a way for library authors to enforce key ordering. If you accept { x: 1, y: 2 } but not { y: 2, x: 1 }, TypeScript should have a type for that. No typechecker for JavaScript is complete unless it can represent this pattern.

Syntax

Initial syntactic proposals for AFD centered around a new top-level declaration, using tyqe with a q (Q) instead of p (P) in type to represent that the fields had to be "in an orderly Queue":

tyqe Ordered = { a: string, b: string }

In our opinion, concerns about reabidility here are misplaced, since modern code editors can use ligatures to replace tyqe with something more customized based on user preference.

However, much to our dismay, multiple TC39 proposals in the works are interested in using this keyword.

We then tried modifying the interface declaration form:

interface Ordered {
    a: string; then
    b: string; then
    c: string;
}

This has some big advantages:

  • then is a familiar keyword to Pascal programmers, a core demographic of TypeScript
  • This would support partial order enforcement (e.g. a before b and x before y, but a x b y would be a legal order), where only some keys would be subject to ordering constraints. People enjoy this level of complexity, and more complexity equals more job security for us. Win/win.
  • Fully backward-compatible as long as no commonly-used JS object has a property called then, and we're not aware of any (TODO: resolve to look into this)

Unfortunately, we hit another roadblock -- Bill Gates has a patent on then following a semicolon back from the Microsoft BASIC days, and no one could get in touch with him to see if he'd license it to us.

In the end, we decided that postfix natural-language operators like as and satisfies have been popular, and adopted that design principle for AFD:

interface Ordered {
    a: string;
    b: string;
    c: string;
} in that order;

This works in both type and object literals, and extends to some other unexpected places we'll see later.

Semantics

The semantics of AFD properties are pretty obvious:

interface OrderedVector {
    x: number;
    y: number;
    z: number;
} in that order;

declare function check(x: OrderedVector): void;

// OK
check({ x: 0, y: 0, z: 0 });

If your properties in are the wrong order, you'll get a helpful error message:

check({ y: 0, x: 0, z: 0 });
//     ~~~~~~~~~~~~~~~~~
// > Error: Object literal is in wrong order
//  > One or more properties 'x' follows an incorrect property key 'z' | 'y'
//    > Property 'x' must precede one or more properties
//      > Property 'y' incorrectly follows property 'x'
//        > Property 'z' is still OK though
//          > Property slot '0' must be 'x'
//            > Help I'm stuck in an error message factory
//              > Property slot '1' must be 'y'

You can also use AFD in union types, solving a longstanding problem around the supposed "unorderability" of sets:

type HelloOrWorld = "hello" | "world" in that order;

When a union is ordered, code logic must conform to the union ordering:

// OK: 'hello' preceded 'world'
const hw1: HelloOrWorld = Math.random() > 0.5 ? "hello" : "world";

// Logic error detected!
const hw2: HelloOrWorld = Math.random() <= 0.5 ? "world" : "hello";
//         ~~~~~~~~~~~~
// > Error: Ordered union provided in the wrong order
//   > Type "world" must succeed "hello"
//     > Did you mean 'secede'? 'succeed' refers to doing well
//       > No, 'secede' means to withdraw. 'Succeed' is the right word here
//         > Yeah you're right. What a country!
//           > Specify `--skipSucceedElaboration` to silence this error
//             > Specify `--skipSkipSecedeElaboration` to suppress the suggestion to skip this message

Compatibility

AFD will ship alongside with the compatibility flags --noAFD , --noImplicitAFD, --strictAFD, --isolatedAFD, and the temporary transition flag --noUncheckedExperimentalInThatOrder which we'll try to unsuccessfully try to deprecate in TypeScript 8.0.

Feedback

Please check this feature out when it becomes available, evaluate it, and send us feedback (in exactly that order)!

@RyanCavanaugh RyanCavanaugh added the Experimentation Needed Someone needs to try this out to see what happens label Apr 1, 2024
@MartinJohns
Copy link
Contributor

--noAFD

As a German I strongly support this flag.

@rubiesonthesky
Copy link

I think this should get label Great Suggestion of something similar. It would be great to tag some great suggestions from past years with that same label so it would be easier to find them!

@IllusionMH
Copy link
Contributor

IllusionMH commented Apr 1, 2024

Finally! 👍 This will make my life (huge regexp based code changes across tens or hundreds of files) easier as TS would solve problems with properties order.

Only two points:

  • Could we still merge ordered and unordered interfaces? Are there any special requirements
  • Will it finally "solve" magic overloads priorities (e.g. bubbling of literals)?

For later I can see how it would "fix" it with in that order if supported.

interface OrderedOverloads {
   check(val: string, length?: number): boolean;
   check(val: 'works'): number;
} in that order;

Could you please add it in scope of this proposal?! 🙏

@snarbies
Copy link

snarbies commented Apr 1, 2024

How will this interact with mapped types? I'm trying to work out how I can write some helper types. Will something like this be available?

type Scrambled<T> = { [k in keyof T]: T[k] } in random order

@jcalz
Copy link
Contributor

jcalz commented Apr 1, 2024

Thank goodness you folks finally came to your senses. Now we have a firm foundation for JSON.stringify()-based equality checking, as well as a path toward supporting important feature requests like #13298. Probably with just a few more changes to the way template literals work it could be written simply in userland as

type UnionToTuple<T> = `[${T in that order}]` extends `${infer U extends any[]}` ? U : never;

where T in that order uses the same principle as const arr = ["a", "b", "c"]; const orderedArr = arr as const to retrieve information from the trash bin before it gets picked up by the language's garbage collector.

It even lets me amend my answer to a recent question about the full set of meanings for the TypeScript in operator. Seriously, kudos.

My only concern is that you've introduced the that keyword, and I'm a little worried it'll interact badly with my upcoming proposal for polymorphic that, which would solve #41181 as follows:

interface Cloneable { clone(): that }
class A implements Cloneable {
    constructor(readonly a: number) { }
    clone(): A { return new A(this.a) } // okay now
}

@RyanCavanaugh
Copy link
Member Author

I forgot that (under noImplicitAFD) this modifier is also applicable to string literals, with the default behavior being unordered strings

type T = "typescript rocks";
const p: T = "tricky prospects"; // OK (anagram subtyping)

type U = "typescript is cool" in that order;
const q: U = "cryptic stool piles"; // Error, letters are not in the right order

@fatcerberus
Copy link

Wait that doesn't sound right, clearly { 0: "f", 1: "o", 2: "o" } is never equivalent to { 0: "o": 1: "o", 2: "f" }, even with unordered keys

@josh-degraw
Copy link

josh-degraw commented Apr 1, 2024

Help I'm stuck in an error message factory

LGTM

@rubiesonthesky
Copy link

Wait that doesn't sound right, clearly { 0: "f", 1: "o", 2: "o" } is never equivalent to { 0: "o": 1: "o", 2: "f" }, even with unordered keys

This went from foo to oof quickly.

@nmain
Copy link

nmain commented Apr 2, 2024

This makes a difference in V8 deopt, and could be useful to have in a linter or typechecker in some form, even if the proposal here is more for laughs.

@kurtextrem
Copy link

To expand on what @nmain says, see e.g. here: https://romgrk.com/posts/optimizing-javascript/#2-avoid-different-shapes by @romgrk or the really in-depth article he links to: https://mrale.ph/blog/2015/01/11/whats-up-with-monomorphism.html

@jcalz
Copy link
Contributor

jcalz commented Apr 2, 2024

I, too, long for the following fine day:

interface Foo {
  a?: string,
  b: number;
} in that order;
const foo: Foo = { b: 123 }; // error, maybe?
foo.a = "abc"; // error, probably!

@kieselai
Copy link

I was looking for some kind of Sorted intrinsic utility for tuples and stumbled on this... I hate it soooo much 😆

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Experimentation Needed Someone needs to try this out to see what happens
Projects
None yet
Development

No branches or pull requests