-
Notifications
You must be signed in to change notification settings - Fork 12.6k
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
Google feedback on TypeScript 3.5 #33272
Comments
|
FWIW, Apart from backward compatibility issues (or, "ignoring the elephant in the room"), I'd be happy with much more widespread use of |
A better default (in my opinion) would be an existential type that is I wouldn't consider This is safe, declare function expectStringSet(s : Set<string>) : void;
//OK
expectStringSet(new Set());
declare function expectString(s : string) : void;
//Error
expectString(new Set()); This is safe, declare function expectStringPromise(s : Promise<string>) : void;
//OK
expectStringPromise(new Promise((resolve) => {
resolve("str") //OK!
resolve(42) //Error!
}));
declare function expectString(s : string) : void;
//Error
expectString(new Promise(() => {})); With a "return-only generic", we can't even reason about the shape of the return type. It's just... A type that we know exists. This is never safe, declare function returnOnlyGeneric<T> () : T;
//OK
declare function expectStringSet(s : Set<string>) : void;
expectStringSet(returnOnlyGeneric())
//OK
declare function expectString(s : string) : void;
expectString(returnOnlyGeneric());
//OK
//???
declare function expectNever(n : never) : void;
expectNever(returnOnlyGeneric());; |
@DanielRosenwasser if you or anyone on your team who was interested read this, feel free to close it; I don't think it's actionable as a bug. (Also lemme know if it was useful or not, and we can do a similar one for 3.6.) |
I think it's useful from a community perspective - a lot of other TS users around the world likely hit (or will hit) similar difficulties when upgrading, and having some extra documentation on the issues one might encounter could be tremendously useful. I've somewhat frequently hit rather arcane-feeling errors with complex react types when upgrading TS and wished that there was a more detailed guide to what's contained in the latest version (though the official release notes are very well done). Perhaps there's some community-curated form of this "TypeScript upgrade challenges and solutions" list that could be created, but even if that never happens, I think your notes are worth posting as a resource to others, so thank you! |
@evmar I imagine y'all needed to use codemods for this... what do you use to codemod? I've been using jscodeshift but find it to be pretty sparsely documented. |
I too found the implicit |
Hey @evmar, thanks a ton for writing this up. Having this available publicly is great, and gives us an easy way to return to this discussion, so I especially appreciate that. It sounds like users outside of both Google and Microsoft have gotten something out of it too! This isn't the first batch of feedback that we've leveraged either. For example, with certain breaks, we've tried to stretch them out over several releases. But for what it's worth, I think that getting this sort of feedback earlier on would be more ideal since we would have realized how impactful each of these changes were (and whether they warranted a flag). While we can err on the side of caution and always introduce flags and the like, that's still more cognitive overhead we'd generally rather avoid. We've actually expanded our release cadence in 3.6 to be longer in order to encourage users (and larger organizations like Google) to have more time to upgrade and try betas and RCs. I think that the sooner you can upgrade to 3.6 and hopefully the 3.7 beta, the better off the community as a whole will be too. Let us know if there's anything we can do to help there! |
Thanks @DanielRosenwasser, the slower cadence is definitely helpful. And it really is on us to provide more timely feedback; I totally get that it's not super useful to hear about stuff that are N+2 revs old. We keep getting preempted by other projects but are hoping to catch up soon, and we started on 3.6 basically immediately after we finished 3.5 (see e.g. #33295). |
Usually people use lerna for a monorepo. It creates symlinks but allows you
to reference other projects as though they were in npm.
@google/a
References
@google/b
But in your local workspace you have
packages/a/package.json
And
packages/b/package.json
…On Tue, Sep 10, 2019, 3:56 PM Lee Henson ***@***.***> wrote:
@evmar <https://github.com/evmar> I'd be really interested to know how
you organise your TS in your monorepo. Are you using shared packages,
symlinks, project references etc? Probably OT for this thread, but if you
could find the time maybe you could post something in #25376
<#25376> ? Thanks
—
You are receiving this because you commented.
Reply to this email directly, view it on GitHub
<#33272?email_source=notifications&email_token=AAQGPCSHVBG6LC72JJ3PZLTQI73VBA5CNFSM4IUCAKO2YY3PNVWWK3TUL52HS4DFVREXG43VMVBW63LNMVXHJKTDN5WW2ZLOORPWSZGOD6MJNOY#issuecomment-530093755>,
or mute the thread
<https://github.com/notifications/unsubscribe-auth/AAQGPCTWL3CLLDLBGCGWRMDQI73VBANCNFSM4IUCAKOQ>
.
|
@evmar Are you able to share Google TS adoption as of this 3.5 upgrade milestone (% of TS relative to total JS)? I'm interested in hearing more about others' large-scale adoption efforts/progress. Since I'm the one asking, I'll go first. At Bloomberg, we have a JS footprint in the broad range of "tens of millions" LoC. Within that footprint, we are now roughly 6% TS adoption by LoC, 8% TS adoption by file count. |
See bazelbuild/rules_nodejs and bazelbuild/rules_typescript. tl;dr Google uses a polyglot build tool Bazel to orchestrate. |
This bit me not because of DI, but because of a factory pattern. I have a method that takes a abstract class Base<T> { ... }
class Sub<T> extends Base<T> { ... }
function create<T>(clazz: Ctor<T>): T { ... }
function consume<T>(instance: Base<T>) { ... }
const x = create(Sub);
consume(x); // Error! broke for the reasons described in the OP. I "fixed" this by liberally peppering the generic parameters with default |
@rkirov and @bowenni gave on how we use Typescript in Google at TSConf 2018 https://www.youtube.com/watch?v=sjov1k5jexA |
Interesting, but I was really hoping for some discussion about how they do package management and link their local packages together.... |
@leemhenson I really think you need to look into lernajs https://lerna.js.org/ I use it at my job, and it's great. I have a root tsconfig in the {
"files": [],
"references": [
{ "path": "pkg1" },
{ "path": "pkg2" },
{ "path": "pkg3" }]} (You can then build all packages with a Each project then references my base tsconfig {
"compileOnSave": false,
"compilerOptions": {
"allowJs": false,
"allowSyntheticDefaultImports": true,
"allowUnreachableCode": false,
"allowUnusedLabels": false,
"alwaysStrict": true,
"declaration": true,
"declarationMap": true,
"disableSizeLimit": true,
"emitBOM": false,
"emitDeclarationOnly": false,
"emitDecoratorMetadata": false,
"esModuleInterop": true,
"experimentalDecorators": true,
"forceConsistentCasingInFileNames": true,
"importHelpers": true,
"incremental": true,
"isolatedModules": false,
"lib": ["dom", "es2015", "es2016", "es2017.object"],
"module": "esnext",
"moduleResolution": "node",
"newLine": "LF",
"noEmitOnError": true,
"noErrorTruncation": true,
"noFallthroughCasesInSwitch": true,
"noImplicitAny": true,
"noImplicitReturns": true,
"noImplicitThis": true,
"noStrictGenericChecks": false,
"noUnusedLocals": true,
"preserveConstEnums": false,
"preserveSymlinks": true,
"pretty": true,
"removeComments": false,
"resolveJsonModule": true,
"sourceMap": true,
"strict": true,
"strictBindCallApply": true,
"strictFunctionTypes": true,
"strictNullChecks": true,
"strictPropertyInitialization": true,
"stripInternal": true,
"suppressExcessPropertyErrors": false,
"suppressImplicitAnyIndexErrors": false,
"target": "esnext"
},
"exclude": ["./node_modules/"]
} with something like {
"compilerOptions": {
"composite": true,
"jsx": "preserve",
"outDir": "lib",
"rootDir": "src"
},
"exclude": ["./lib/", "./node_modules/"],
"extends": "../tsconfig.base.json",
"references": [{ "path": "../communication" }, { "path": "../utilities" }]
} Lerna makes symlinks for all your packages so you still reference the other packages with scoped package names like |
I know about lerna, I use pnpm for similar. I'm interested in hearing how google do it with monorepo as large as theirs. |
@leemhenson The linked video talks about how Google uses Bazel, to build everything from head so there's no versioning concerns https://youtu.be/sjov1k5jexA?t=130 That bit starts at 2:10 on the video |
This difficulty in migrating implicit defaults for generics looks like an argument for #26242 to me. Specifically, this developer behavior:
Since in generic types with several parameters, there is not a way to specify one parameter but leave others inferred, developers are forced to specify all of them. (When a default for the type parameter is not viable.) Then if the inferred type changes, even in a way not relevant to that code, that site needs to be updated. Supporting #26242 would at least allow a mechanism for the developer to limit this. Laziness would then lead to a "pit of success" where developers would leave the last types unspecified rather than blindly copying the inferred type into the code. |
The proposals, at the moment, are different. That proposal is about partial type argument inference for function/method calls. It seems like what you really want is partial type argument inference for explicit type annotations. const mySel: d3.Selection<HTMLElement, infer, null, undefined> = /*blah*/; However, that doesn't really fix anything. When you hover over the return type of Then, you copy-paste it as an explicit type annotation. Now, you have an If it is the default of an unconstrained type parameter, then you want to replace that If it isn't the default and is, in fact, explicit, then you want to leave it as It just seems like a non-solution, to me. I can only reasonably see the default constraint type changing once more (if at all) in the future, so it isn't too bad, right? I'm keeping my fingers crossed for existential types and hoping it'll be the new default constraint type |
We recently upgraded Google to use TypeScript 3.5. Here is some feedback on the upgrade.
(For background, recall that Google is a monorepo of billions of lines of code. We use a single version of TypeScript and a single set of compiler flags across all teams and upgrade these simultaneously for everyone.)
We know and expect every TypeScript upgrade to involve some work. For example, improvements to the standard library are expected and welcomed by us, even though they may mean removing similar but incompatible definitions from our own code base. However, TypeScript 3.5 was a lot more work for us than other recent TypeScript upgrades.
There were three main changes in 3.5 that made it especially painful. (The other changes were also required work, but these three are worth some extra discussion.) We believe most of these changes were intentional and intended to improve type checking, but we also believe the TypeScript team understands that type checking is always just a tradeoff between safety and ergonomics.
It is our hope that this report about TS 3.5 as applied to a large codebase will help the TypeScript team better evaluate future situations that are similar, and we make some recommendations.
Implicit default for generics
This was the headline breaking change in 3.5. We agree with the end goal of this change, and understand that it will shake up user code.
Historically when TypeScript has introduced type system changes like this, they were behind a flag.
Suggestion: Using a flag here would have allowed us to adapt to this change separately from the other breaking changes in 3.5.
The main way this failed is in circumstances where code had a generic that was irrelevant to what the code did. For example, consider some code that has a Promise resolve, but doesn't care about what value the Promise to resolves to:
Because the generic is unbound, under 3.4 this was
Promise<{}>
and under 3.5 this becomesPromise<unknown>
. If a user of this function wrote down the type of that promise anywhere, e.g.:it now became a type error for no user benefit.
The bulk of churn from this generics change was in code like this, where someone wrote a
{}
mostly because it was what the compiler said without really caring what type it was.One common concrete example of this don't-care pattern are the typings for the
d3
library, which has a very complexd3.Selection<>
that takes four generic arguments. In the vast majority of use cases the last two are irrelevant, but any time someone saves aSelection
into a member variable, they ended up writing down whatever type TS inferred at that time, e.g.:The 3.5 generics change means that
{}
becameunknown
simultaneously in almost every interaction with d3.Suggestion: Our main conclusion about specifically d3 is that the d3 typings are not great and need some attention. There are some other type-level issues with them (outside of this upgrade) that I'd like to go into more, but it's not relevant to this upgrade.
Another troublesome pattern are what we call "return-only generics", which is any pattern where a generic function only uses it in the return type. I think the TypeScript team already knows how problematic these are, with lots of inference surprises. For example, in the presence of a return only generic, the code:
can be legal while the innocent-looking refactor
can then fail.
Suggestion: We'd be interested in seeing whether TypeScript could compile-fail on this pattern entirely, rather than picking a top type (
{}
orunknown
). Users are happy to specify the generic type at the call site, e.g.myFunction<string>()
but right now the compiler doesn't help them see when they need it. For example, maybe the declarationmyFunction<T>(...)
could always require a specificT
to be inferred, because you can always writemyFunction<T=unknown>()
for the case where you are ok with a default.One other common cause of return-only generics is a dependency injection pattern. Consider some test framework that provides some sort of injector function:
where
Ctor<T>
is some type that matches class values. The intended use of this is e.g.This works great up until
MyService
is generic, at which point this again picks an arbitrary<T>
for the return type. The problem here is that we pass theMyService
value togetService
, but then we get back theMyService
type, which needs a generic.One last source of return-only generics that we discovered is that the generic doesn't need to be in the return type. See the next section.
filter(Boolean)
TypeScript 3.5 changed the type of the
Boolean
function, which coerces a value toboolean
, from (effectively)to
These look like they might behave very similarly. But imagine a function that takes a predicate and returns an array filter, and using it with the above:
With the 3.4 definition of Boolean,
T
is pinned toany
andmyFilter
becomes a function fromany[]
toany[]
. With the 3.5 definition,T
remains generic.We believe this change was intentional, to improve scenarios like this.
The RxJS library uses a more complex variant of the above pattern, and a common use of it creates a function composition pipeline with a
filter(Boolean)
much like the above. With TS 3.4, users were accidentally gettingany
downstream of that point. With TS 3.5, they instead get a genericT
that then feeds into a larger inference. You can read the full RxJS bug for some more context.One of the big surprises here is that everyone using RxJS was getting an unexpected
any
at this point. We knew to look forany
in return types, but now we know that even if you accept anany
in an argument type, via inference this can cause other types to becomeany
.Suggestion: A more sophisticated definition of
Boolean
, one that removed null|undefined from its generic, might have helped, but from our experiments in this area there are further surprises (search for "backwards" on the above RxJS bug). This was also not mentioned in the list of breaking changes in 3.5. It's possible its impact was underestimated because it disproportionately affects RxJS (and Angular) users.Set
In TypeScript 3.4,
gave you back a
Set<any>
. (It's actually a kind of amusing type, because in some sense almost everything still works as expected -- you can put stuff in the set,.has()
will tell you whether something is in the set, and so on. I suspect this might be why nobody noticed.)TypeScript 3.5 made a change in
lib.es2015.iterable.d.ts
that had the effect of removing theany
, and the generic change described above made it now inferunknown
.This change ended up being tedious to fix, because the eventual type errors sometimes were pretty far from the actual problem. For example, in this code:
You get a type error down by the
Array.from
but the required fix is at thenew Set()
. (I might suggest the underlying problem in this code is relying on inference too much, but the threshold for "too much" is difficult to communicate to users.)Suggestion: we are surprised nobody noticed this, since it broke our code everywhere. The only thing worth calling out here is that it seems like nobody made this change intentionally -- it's not in the breaking changes page, and the bug I filed about it seems to mostly have prompted confusion. The actual change that I think changed what overloads got picked looks harmless. Perhaps the main lesson we learned here is that we needed to discover this earlier and provide this feedback earlier.
PS: It also appears
new Map()
may have the same problem withany
.Conclusion
I'd like to emphasize we are very happy with TypeScript in general. It is our hope that the above critical feedback is useful to you in your design process for future development of TypeScript.
The text was updated successfully, but these errors were encountered: