Typescript: Design Meeting Notes, 12/20/2019

Created on 20 Dec 2019  路  1Comment  路  Source: microsoft/TypeScript

A Stricter Subtype Relationship

  • Issue: https://github.com/microsoft/TypeScript/issues/35414
  • PR: https://github.com/microsoft/TypeScript/pull/35659

  • We have this thing where we perform union subtype reduction - reducing constituents of unions when a union contains a supertype.

  • But we sort unions by a type IDs, but that's not a "stable" ordering.

    • Our current subtype relationship contains "ties" though - several types can be subtypes of each other.

    • First type ID wins, so if you reorder declarations/instantiations in your program, the behavior changes! That's undesirable.

  • The solution is to avoid places where types can "tie" in which types are supertypes.
  • It's strictly a good thing in subtype reduction, but it can have effects elsewhere.

    • One effect that's measured is that two constituents might be preserved in a reduced union (something we're willing to take).

    • Additionally, because of our union merging, we might have different effects on which signatures are present.

    • Also seeing differences in overload resolution because overload resolution does a subtype pass.



      • Historical reasons for saying any is not the best type to choose from.



  • These changes introduce a lot of potential breaks elsewhere, so we introduced a separate relationship to the compiler.

    • Ideally we'd use this new subtype relationship everywhere, but we don't want to impact too much.

  • Example breaks seem "good" but hard to deal with.
  • Could we imagine a strict mode that uses the strict subtype relationship?

    • Maybe! It's worth thinking about.

  • As a compiler writer, what relationship do I use?

    • [[rules that went by too fast, but you can consult with the team]]

  • Conclusion

    • Turn it on just for subtype reduction.

    • Try it out everywhere, see what breaks, see if it can fit under a strict mode - i.e. --strictSubtypes but with a better name.

Assume Changes Only Affect Dependencies

Type-Only Imports/Exports

https://github.com/microsoft/TypeScript/pull/35200

  • Scenarios

    • Re-export a type in isolatedModules

    • Stop eliding imports when I do DI

    • *
  • Behavior

    • Changed default emit behavior to preserve imports

    • Flag to opt in to the old behavior

  • Effect

    • Can increase bundle sizes 馃憥

    • Hard to determine where to fix 馃憥

    • Runtime behavior might break, or you might not notice the change at all 馃憥

  • Proposals to mitigate issues

    • Proposal 1



      • Add error on regular imports used only as types.


      • Disable error via a flag.



    • Proposal 2



      • Keep today's emit behavior.


      • Opt into preserved imports via a flag


      • Optionally enable this in tsc --init


      • Optionally add a flag to force type-only imports for identifiers used only as types



    • Proposal 3



      • Transition plan



  • 3 years from now where do we want to be?

    • Can we remove things from the import list under isolatedModules currently?



      • Yes


      • Anecdotal from Wesley: when I transitioned our unit tests into modules, there were a lot of issues because our unittests were only being included via type-imports.


      • Experience is that elision has undesirable behavior



    • This is a very big breaking change even if you phase it over several releases.



      • People are in an unknown state of what they want - only make noise (i.e. give an error) when the intent isn't clear.



  • Can this be a strict mode flag?

    • Strict flags have never affected emit.

  • Could this be mitigated with a tristate flag?

    • Well then what's the default?

  • Conclusion:

    • The safer proposal 2



      • Leaves our options open



    • Implement option as a tristate

    • preserve be default in tsc --init

    • Send PRs to 3rd party tools



      • Turn it on in scaffolding tools


      • Babel





        • Need to signal to the team, maybe need a flag (@hzoo).






Use Getters for Live Bindings

https://github.com/microsoft/TypeScript/pull/33587

  • Turns out even we were giving people "wrong" exports because of this.
  • Let's do it.
Design Notes

Most helpful comment

As a compiler writer, what relationship do I use?

  • [[rules that went by too fast, but you can consult with the team]]

It was

  • for relationships that are somehow exposed to the user, use assignability
  • for internal processing that needs to produce stable-ordered results because you鈥檙e doing something algebraic, use subtype / strict subtype
  • for loose association, use comparability

I recently convinced myself of why the subtype relation is necessary for narrowing when there are some any properties involved, and I think it鈥檚 a good, understandable example. Consider two types:

interface User {
    name: any;
}

interface SpecialUser extends User {
    name: string;
    age?: number;
}

We intuitively understand SpecialUser to be more specific/narrow than User, but the two types are assignable to each other because of name: any. So imagine narrowing the type of some object like this:

declare let someone: unknown;
if (isUser(someone) && isSpecialUser(someone)) {
    someone.age;
}

We could simply use User & SpecialUser, but then a) someone.name would be any and we can do better than that, and b) the intersection is fairly useless in general, and it would be nice not to have to carry it around. So we may as well see if one of these two types is sufficient for someone before falling back to the intersection. If we try to use assignability here, we鈥檙e going to end up picking an arbitrary one, or not getting any useful info, because each is assignable to the other. Assignability is incapable of telling us which type is a better fit. But the subtype relation treats any like a top type, so it can reliably tell that SpecialUser is a subtype of User, but not the other way around. (Playground)

>All comments

As a compiler writer, what relationship do I use?

  • [[rules that went by too fast, but you can consult with the team]]

It was

  • for relationships that are somehow exposed to the user, use assignability
  • for internal processing that needs to produce stable-ordered results because you鈥檙e doing something algebraic, use subtype / strict subtype
  • for loose association, use comparability

I recently convinced myself of why the subtype relation is necessary for narrowing when there are some any properties involved, and I think it鈥檚 a good, understandable example. Consider two types:

interface User {
    name: any;
}

interface SpecialUser extends User {
    name: string;
    age?: number;
}

We intuitively understand SpecialUser to be more specific/narrow than User, but the two types are assignable to each other because of name: any. So imagine narrowing the type of some object like this:

declare let someone: unknown;
if (isUser(someone) && isSpecialUser(someone)) {
    someone.age;
}

We could simply use User & SpecialUser, but then a) someone.name would be any and we can do better than that, and b) the intersection is fairly useless in general, and it would be nice not to have to carry it around. So we may as well see if one of these two types is sufficient for someone before falling back to the intersection. If we try to use assignability here, we鈥檙e going to end up picking an arbitrary one, or not getting any useful info, because each is assignable to the other. Assignability is incapable of telling us which type is a better fit. But the subtype relation treats any like a top type, so it can reliably tell that SpecialUser is a subtype of User, but not the other way around. (Playground)

Was this page helpful?
0 / 5 - 0 ratings