In my opinion the most important factor was that it is a superset of JS and any valid Javascript code is/was valid TS code.
That allowed for gradual adoption and you didn't have to risk going all-in into new technology.
Also the `any` type [1]. It makes porting an existing project ridiculously easy: just type everything as `any`, and it also gives freedom to the developer to think in code when developing, trusting that they (will) know what they are doing, instead of screaming at the smallest mis-type with annoying bright red squiggly lines.
It is so liberating to type a variable as `any` when sketching new code, figuring things out, that I would venture to say that any language not having the `any` type actually actively hates the developer.
And then you have to contribute to a codebase with restrictive ESLint (bleurgh!) configuration, so you can't even try out whether your code works because the tooling disallows you from compiling it as long as it contains "any", and the other devs are like "but muh best practices". So not only you gotta work around TS, you gotta do it invisibly. How did people even live before VSCode's type hint popups covered up the previous line?
No, cringing at some "helpful" popup appearing right over the previous line of code. If anything, they could've made it appear under the current line; code's still written top to bottom so it's less likely for "suggested relevant info" to obscure the actual relevant info.
TS is emphatically not a superset of JS. It rejects perfectly valid JS leaving you no recourse other than design your whole architecture around what TypeScript allows. Which is what Microsoft wants of course.
You can directly import any valid JS module using TypeScript if you have “allowJS”: true in the compiler options of your tsconfig.json file. The comparison to FFI doesn’t make any sense. FFI requires compiling a special library that explicitly exports the C types. This is different than TS. Using TS, you can import any valid JS module without any special preparation. Also, you can’t import all compiled languages into each other using FFI. You can only import and export C-based types. There is no way to export a Go struct for consumption via FFI, for instance. All valid JS modules will work with TypeScript.
$ echo "export class Foo { a = 1 }" > test.mjs
$ npx ts-node
> import('./test.mjs').then(console.log)
error TS7016: Could not find a declaration file for module './test.mjs'. '/home/user/Lab/test.mjs' implicitly has an 'any' type.
$ npx ts-node -O '{"allowJs":true}'
> import('./test.mjs').then(console.log)
Promise { <pending > }
Error [ERR_REQUIRE_ESM]: require() of ES Module /home/user/Lab/test.mjs not supported.
$ echo "module.exports.Foo = class Foo { a = 1 }" > test.cjs
$ npx ts-node -O '{"allowJs":true}'
> const { Foo } = require('./test.cjs')
> Foo
[class Foo]
> new Foo
Foo { a = 1 }
> function bar (foo: Foo) {}
TS2749: 'Foo' refers to a value, but is being used as a type here. Did you mean 'typeof Foo'?
No I did not, TypeScript, and that's about as much as it seems to interoperate. I guess in some cases practical migration might be viable - but as for the general case, the out-of-the-box experience seems to speak otherwise.
It's not even viable to write in JS and manually write a DTS - no way to check em against each other. Except idk library in JS, test suite in TS anyway. So not a lot of lateral movement possible in practice.
I'm not even talking about the module support. (It just seems to have the default support of TypeScript.)
Yes, it imports the module. No, it doesn't do even basic type inference (knowing that Foo is a class and consequently allowing it to be used as a type name) - which it would, if it was a superset of JS. Instead it seems to import everything as "any", which is... a start, I guess?