Strangely enough it was Vite which significantly lowered the adoption barrier for me (I don't use VS Code).
esbuild's transform is so fast and the "To hell with the TypeScript errors, lets run this code now!" approach means that I don't constantly get stalled having to explain (right now, this very moment) to TypeScript what I'm doing. It's only once I get far enough into the weeds that I switch gears and go into "type linting" mode to shore up the work already done.
I do think there are two significant milestones in TypeScript competence:
type consumer
type producer
It's fairly easy to get to the "type consumer" level and that's typically all that's required when duct-taping dependencies together to "make a thing".
Maintainers of dependencies however need to aspire to the "type producer" level and that is quite a bit more work because TypeScript has a lot of concepts that other statically typed languages simply don't need (they aren't trying to bridge the gap to a dynamically typed language where it isn't unusual (and sometimes even desirable) for types to morph at run time).
The less you rely on dependencies and the more you are crafting bespoke capabilities, the more you need to push into "type producer" territory.
The one thing I still don't like is the pressure TypeScript places on coding style. Just recently I ended up this the tight ball of code:
asyncfunctionrootAndRun<T>(timeoutMs:number,factory:Factory<T>):Promise<T>{letdisposeFn:(()=>void)|undefined;lettimeoutId;try{returnawaitnewPromise((resolve,reject)=>{createRoot((dispose)=>{disposeFn=dispose;timeoutId=setTimeout(functiontimeout(){timeoutId=undefined;reject(newError('Timed out'));},timeoutMs);// queueMicrotask/setTimeout allows `setup` to finish// before exercising the reactive graph with `run`construn=factory(functiondone(data,err){if(data===undefined)reject(err);elseresolve(data);});if(typeofrun==='function')queueMicrotask(run);});});}finally{if(disposeFn){disposeFn();disposeFn=undefined;}if(timeoutId){clearTimeout(timeoutId);timeoutId=undefined;}}}
The JavaScript version was neatly pulled apart into separate functions
asyncfunctionrootAndRun(timeoutMs,factory){letdisposeFn;lettimeoutId;try{returnawaitnewPromise(executor);}finally{if(disposeFn){disposeFn();disposeFn=undefined;}if(timeoutId){clearTimeout(timeoutId);timeoutId=undefined;}}// ---functionexecutor(resolve,reject){createRoot((dispose)=>{disposeFn=dispose;timeoutId=setTimeout(timeout,timeoutMs);// queueMicrotask/setTimeout allows `setup` to finish// before exercising the reactive graph with `run`construn=factory(done);if(typeofrun==='function')queueMicrotask(run);});// ---functiontimeout(){timeoutId=undefined;reject(newError('Timed out'));}functiondone(data,err){if(err!=undefined)reject(err);elseresolve(data);}}}
When I started typing it, TypeScript was constantly complaining how the same T could end up being different types. It's only once I inlined the functions that TypeScript finally "got it".
So when people claim that TypeScript makes refactoring easier they simply mean that it (likely) rings the alarm bells when something doesn't quite line up after you moved some code around.
However I claim that TypeScript doesn't encourage refactoring—in the sense of authoring code that is broken down into easily digestible chunks.
TypeScript's type inference pushes you toward an "inlined" coding style (which I absolutely despise) because inlined code is easier on the type inference. If you want to break things down into sensibly named and sized chunks it penalizes you with a hefty explicit typing tax which most people aren't willing to pay—leading to heavily inlined code which the JavaScript ecosystem already has enough of.
Clarification before you read the rest: I'm not nitpicking, I just found interesting that you said that TS forced you to do something you wouldn't do with JS and I decided to code this just for fun:
The one thing I still don't like is the pressure TypeScript places on coding style.
I kinda disagree with this. The example you provided can be separated into different functions with smaller responsibilities, that could be reused a lot in the rest of your app:
import{createRoot}from"solid-js";/**
* We use this type when we need a promise executor to be passed around.
*/typeExecutor<Value>=(resolve:(value:Value|PromiseLike<Value>)=>void,reject:(reason:unknown)=>void,)=>void;/**
* Classic util function to have a timeout promise.
*/constwait=(milliseconds:number)=><Value>(executor:Executor<Value>)=>newPromise((resolve,reject)=>setTimeout(executor,milliseconds,resolve,reject),);/**
* Making use of `wait`, this function rejects with an error if the time runs out.
*/constcreateTimeoutPromise=(milliseconds:number)=>{consttimeout=wait(milliseconds);return<Value>(promise:Promise<Value>)=>Promise.race([timeout((_resolve,reject)=>reject(newError("Timed out"))),promise,]);};/**
* Promise wrapper for `createRoot`.
*/constcreateRootPromise=<Value>(executor:Executor<Value>)=>{letrootDispose:(()=>void)|undefined;returnnewPromise((resolve,reject)=>{createRoot(dispose=>{rootDispose=dispose;executor(resolve,reject);});}).finally(rootDispose);};/**
* Queues a value that might be a function as a microtask, if it isn't a
* function we just ignore it.
*/constqueueFunction=(maybeFunction:unknown)=>typeofmaybeFunction==="function"?queueMicrotask(maybeFunctionasVoidFunction):undefined;/**
* Finally your function makes use of all the above. I made it curried so you
* can use the same timeout for several factories if you want.
*/constrootAndRun=(milliseconds:number)=>{consttimeoutPromise=createTimeoutPromise(milliseconds);return<Value>(factory:Factory<Value>)=>timeoutPromise(createRootPromise((resolve,reject)=>queueFunction(factory((data,error)=>data===undefined?reject(error):resolve(data),),),),);};
If you take a look, you'll notice that every function and type there can be reused to solve other problems, except rootAndRun.
However I claim that TypeScript doesn't encourage refactoring—in the sense of authoring code that is broken down into easily digestible chunks.
TypeScript's type inference pushes you toward an "inlined" coding style (which I absolutely despise) because inlined code is easier on the type inference.
I also dislike having everything in the same function inlined, but really like to make as much use as possible of type inference. I hope the above example is enough to show that we can separate things quite a bit without any issue. And that is not much about JS/TS, but more about making each function do less, but do it right.
That was actually pretty fun to code 😄 ... cheers!
"The caller of the function specifies the type parameter, not the implementer"
If you refer back to my TypeScript version please note that I require Promise<T>.
Your version only gives me Promise<unknown>. The moment I put Promise<T> on rootAndRun() everything lights up red. The Promise<T> based on Factory<T> was the point of making rootAndRun() generic.
For Promise<unknown> we don't need generics to begin with. CodeSandbox
unknown is extremely useful under very specific circumstances but when overused it reminds me of the Java pre-Generic Object fiasco in terms of type safety.
that could be reused
How OO of you 😁
In this case reuse isn't the goal as this is only for testing support, so not being coupled via reuse (Beware of the Share) is a good thing.
Also in general I try to stick to the Rule of Three:
You must have looked at at least three systems to understand what is common across them (and therefore reusable)
It takes three times as much effort to make something reusable as to make it usable
You will receive payback after the third release.
In this case the goal was to "unroll" it enough to make it more understandable.
Here's your CodeSandbox with the minimum fixes I would do to it. The problems I saw:
executor had its own T instead of using the one from rootAndRun and Factory, so that will be taken as different generics even if you name them the same. You can see this by just renaming T to something else (F2).
done has the same problem.
resolve inside done expects T, but done actually passes data as T | undefined, so you have to check if data is defined before passing it to resolve.
Other than that I just added Maybe (an alias for Type | undefined that I always have around), and made some changes here and there to make my linter happy (like using error instead of err).
How OO of you 😁
Trust me, FP is all about reuse. I don't follow a "rule of N", I just try to keep my functions small and make them do one job and do it right which makes them far easier to test, maintain and reuse.
So my "type parameters inside closures" needs more work; figures. The Maybe makes sense, I'm constantly surprised it's not with the utility types.
I just try to keep my functions small and make them do one job and do it right which makes them far easier to test, maintain …
I'm right there with you in terms of small and focused but I'm not prescient so it's far too easy for me to fall prey to premature reuse/abstraction—OO or functional.
Thank You for the assist!
For further actions, you may consider blocking this person and/or reporting abuse
We're a place where coders share, stay up-to-date and grow their careers.
Strangely enough it was Vite which significantly lowered the adoption barrier for me (I don't use VS Code).
esbuild's transform is so fast and the "To hell with the TypeScript errors, lets run this code now!" approach means that I don't constantly get stalled having to explain (right now, this very moment) to TypeScript what I'm doing. It's only once I get far enough into the weeds that I switch gears and go into "type linting" mode to shore up the work already done.
I do think there are two significant milestones in TypeScript competence:
It's fairly easy to get to the "type consumer" level and that's typically all that's required when duct-taping dependencies together to "make a thing".
Maintainers of dependencies however need to aspire to the "type producer" level and that is quite a bit more work because TypeScript has a lot of concepts that other statically typed languages simply don't need (they aren't trying to bridge the gap to a dynamically typed language where it isn't unusual (and sometimes even desirable) for types to morph at run time).
The less you rely on dependencies and the more you are crafting bespoke capabilities, the more you need to push into "type producer" territory.
The one thing I still don't like is the pressure TypeScript places on coding style. Just recently I ended up this the tight ball of code:
The JavaScript version was neatly pulled apart into separate functions
When I started typing it, TypeScript was constantly complaining how the same
T
could end up being different types. It's only once I inlined the functions that TypeScript finally "got it".So when people claim that TypeScript makes refactoring easier they simply mean that it (likely) rings the alarm bells when something doesn't quite line up after you moved some code around.
However I claim that TypeScript doesn't encourage refactoring—in the sense of authoring code that is broken down into easily digestible chunks.
TypeScript's type inference pushes you toward an "inlined" coding style (which I absolutely despise) because inlined code is easier on the type inference. If you want to break things down into sensibly named and sized chunks it penalizes you with a hefty explicit typing tax which most people aren't willing to pay—leading to heavily inlined code which the JavaScript ecosystem already has enough of.
Clarification before you read the rest: I'm not nitpicking, I just found interesting that you said that TS forced you to do something you wouldn't do with JS and I decided to code this just for fun:
I kinda disagree with this. The example you provided can be separated into different functions with smaller responsibilities, that could be reused a lot in the rest of your app:
If you take a look, you'll notice that every function and type there can be reused to solve other problems, except
rootAndRun
.I also dislike having everything in the same function inlined, but really like to make as much use as possible of type inference. I hope the above example is enough to show that we can separate things quite a bit without any issue. And that is not much about JS/TS, but more about making each function do less, but do it right.
That was actually pretty fun to code 😄 ... cheers!
TIL: There needs to be a comma after the type parameter for an arrow function otherwise TS thinks it's JSX.
The error message I was fighting:
"Argument of type
T
is not assignable to parameter of typeT
.T
could be instantiated with an arbitrary type which could be unrelated toT
."CodeSandBox
Which seems to imply:
"The caller of the function specifies the type parameter, not the implementer"
If you refer back to my TypeScript version please note that I require
Promise<T>
.Your version only gives me
Promise<unknown>
. The moment I putPromise<T>
onrootAndRun()
everything lights up red. ThePromise<T>
based onFactory<T>
was the point of makingrootAndRun()
generic.For
Promise<unknown>
we don't need generics to begin with. CodeSandboxunknown
is extremely useful under very specific circumstances but when overused it reminds me of the Java pre-GenericObject
fiasco in terms of type safety.How OO of you 😁
In this case reuse isn't the goal as this is only for testing support, so not being coupled via reuse (Beware of the Share) is a good thing.
Also in general I try to stick to the Rule of Three:
In this case the goal was to "unroll" it enough to make it more understandable.
Here's your CodeSandbox with the minimum fixes I would do to it. The problems I saw:
executor
had its ownT
instead of using the one fromrootAndRun
andFactory
, so that will be taken as different generics even if you name them the same. You can see this by just renamingT
to something else (F2).done
has the same problem.resolve
insidedone
expectsT
, butdone
actually passesdata
asT | undefined
, so you have to check ifdata
is defined before passing it toresolve
.Other than that I just added
Maybe
(an alias forType | undefined
that I always have around), and made some changes here and there to make my linter happy (like usingerror
instead oferr
).Trust me, FP is all about reuse. I don't follow a "rule of N", I just try to keep my functions small and make them do one job and do it right which makes them far easier to test, maintain and reuse.
So my "type parameters inside closures" needs more work; figures. The
Maybe
makes sense, I'm constantly surprised it's not with the utility types.I'm right there with you in terms of small and focused but I'm not prescient so it's far too easy for me to fall prey to premature reuse/abstraction—OO or functional.
Thank You for the assist!