A 'CSS reset' for TypeScript, improving types for common JavaScript API's
github.com
github.com
I really like the ‘unknown’ type. In catch statements for example the error is unknown but you can do dynamic checks to see what it is, then typescript lets you treat it that way.
A value whose type we don't know is different from a value which we know could be any type.
Whereas ‘Unknown’ might be a stepping stone to introduce a more specific type once all the use cases are known. I let it handle strings and integers, log a line about any other type that calls it, and continue behaviour as it is today.
For example:
function handleString(s: string) {
//...
}
declare const a: any;
declare const u: unknown;
handleString(a); // works with zero intervention
handleString(u); // error: Argument of type 'unknown' is not assignable to parameter of type 'string'
handleString(u as string); // works again with explicit re-typingunknown = I don’t know
any = I don’t care
Often the implications are the same, but as you can intuit, any is a lot more relaxed.
Any will work anywhere because you just don’t care what the type is.
Unknown is stricter. It won’t work in cases where other types expect you to know what type you’re dealing with.
The compiler treats unknown very cautiously, whereas you can slap an any on pretty much anything.
One thing I love about typescript that I've never seen in other languages is that you can adjust the type checker exactly for the level of rigidness that project at hand needs. Any is okay for 200 LOC quick prototypes.
It’s also just flat out impossible in many other languages because the compiler actually needs to know the types for performance reasons.
Typescript managed to be the best of both worlds, as you can be both lenient when you're still in the design/prototyping phase of a project, and need to test idea quickly, but also add as much type-check as you see fit when you're maintaining a project, and need to interface with lots of existing code.
As such, I'm always a bit miffed when I see so many people adamant on setting it to maximum strictness by default, under the guise of "best practice".
Especially `strictNullChecks`. It's a massive headache to deal with that later on.
If you're writing a prototype (so the code will eventually be thrown away), it's just 200 LOC and you're using `any` for everything, why even use TypeScript? Should just use vanilla JavaScript instead.
And if you wanna break the rule of throwing away prototype code, you can always "upgrade" it to TypeScript later without too much fuzz.
I love using TS for prototypes because you can define the interfaces wherever you feel they're important, and leave the rest to any.
And like you said, you can always improve the types later
The case where I've found this useful is making a quick and dirty prototype that integrates within an existing typescript codebase. Such a prototype would never make it to the main branch but sometimes it's useful to make a quick proof of concept of the the runtime behaviour before comitting to sorting out the types.
You should not need ‘any’ in code written in TypeScript from the beginning.
Writing Typescript well requires some fundamentally different patterns than javascript. Adding types afterward is like pulling out your teeth slowly.
Lets say you have some input json that you want to slightly modify to something else. How would you do this with unknown? I can't just blindly replace any with unknown. I'd get errors like this: The right-hand side of a 'for...in' statement must be of type 'any', an object type or a type parameter, but here has type 'unknown'.ts(2407) For example, how can I do this better?
Remember the input json could be pretty much anything. I don't have a spec other than I only care about things that end with __c.
https://github.com/kusl/salesforcecontactmapper/blob/eff0b3e...
import { Output } from "./Output";
import { Preference } from "./Preference";
export function MyMap(input: unknown): Output {
const mypreferences = Array<Preference>();
for (const prefCode in input) {
if (prefCode.endsWith("__c")) {
if (prefCode === "IsInternalUpdate__c") {
continue;
}
let currentValue = "";
if (input[prefCode] !== null) {
currentValue = input[prefCode].toString();
}
if (currentValue === "true") {
currentValue = "True";
}
if (currentValue === "false") {
currentValue = "False";
}
const preference: Preference = {
PrefCode: prefCode,
CurrentValue: currentValue
}
mypreferences.push(preference);
}
}
const myOutput: Output = {
ContactId: input.Contact__c,
Email: input.ContactEmail__c,
IsInternalUpdate: true,
Preferences: mypreferences
}
return myOutput;
}You could say technically could be simply { "Unsubscribe__c": false } or even {} both of which are silly in my case because there is no key for me to identify who the person is but they are valid inputs.
Or the test case I have is https://github.com/kusl/salesforcecontactmapper/blob/eff0b3e...
Or the input could have a thousand key values and I only care about some of them. What should my object look like? How do I create a class that says everything that ends in "__c" is something I care about? I tried unknown. I tried Object. How do I fix this (and learn something so I fix all future code I write)?
if (input[prefCode] !== null) {
currentValue = input[prefCode].toString();
}
in the lines above, I see a red underline under input[prefCode]> Element implicitly has an 'any' type because expression of type 'string' can't be used to index type 'Object'. No index signature with a parameter of type 'string' was found on type 'Object'.ts(7053)
function getProperty<T, K extends keyof T>(o: T, propertyName: K): T[K] {
return o[propertyName];
}
?If you wanna go down that rabbit hole, I'd suggest Typescript type challenges. Completely blew my mind when I came across it the first time.
When you start out with typescript you might think Omit<> and Partial<> are cool but holy hell, you can do so much more.
input: Record<string, unknown> const myOutput: Output = {
ContactId: input.Contact__c,
Email: input.ContactEmail__c,
IsInternalUpdate: true,
Preferences: mypreferences
}
(property) Output.ContactId: string Type 'unknown' is not assignable to type 'string'.ts(2322) Output.ts(4, 5): The expected type comes from property 'ContactId' which is declared here on type 'Output' // @ts-check
/// <reference path="./ts-reset-0.3.7/src/entrypoints/recommended.d.ts"/>
// = contents of the archive from
// https://github.com/total-typescript/ts-reset/releases
const allCurrencies = /** @type {const} */(["EUR", "USD"]);
/**
* @typedef {typeof allCurrencies[number]} currency
*/
allCurrencies.includes('XXX');
// No 'Argument of type '"XXX"' is not assignable…' error any more.
// Would be that error without ts-reset.
/** @type {currency} */
let foo = 'EUR'; // OK
/** @type {currency} */
let bar = 'XXX'; // that error, expectedAFAIK that's not what you currently can do with TypeScript (except Deno).
1. No build step is an appealing feature for some.
Knowing that what is shipped is what you wrote, not what some transpiler spat out, might have some value.
2. Syntax is also subject of personal preference.
Anecdotally, I personally pretty much prefer having "non-executive content" swept aside the "executive" parts, not mixed together in single expression (when applicable).
I assume it is matter of habit, but even though I use both TS and JS, to this days, over TS
function uppercase (something: string): string {
return something.toUpperCase()
}
I'd really rather prefer seeing the "fluff" (types and comments) separated from the "meat" (actual code): in JS (JSDoc) /**
* @param {string} something - glyphs presumably not (only) from upper case
* @returns {string} all glyphs from upper case, when applicable
*/
function uppercase (something) {
return something.toUpperCase()
}
since I still keep mentally stumbling over those :fluffy chunks in TS. Again, probably mater of habit really.There aren't that many TS features that actually require transpiling unless you want to write bleeding edge JS, and that's still a factor without typescript.
> No build step is an appealing feature for some.
ts-node exists
My question is how do you reuse and test the JS doc signatures? Can I export comments? Will my tests fail if the types mismatch?
JSDoc comments can be checked with TSC and I get squiggly lines in VSCode. It's a major step in the right direction. Obviously tests work the same they always did, there is no runtime type checking just as there isn't in TS.
You can use `tsc` to export the types defined in jsdoc and other projects that import your module will get all the intellisense and type checking as if it had been written in TypeScript.
A recent example is a Chrome extension I wrote for the sake of proving out a feature.
Totally not interested in setting up a build system, dealing with dependencies, type safety, and so forth. I just want to write code and immediately see it run. Having JSDoc is nice to document more than just types, but type hints can be helpful.
> My question is how do you reuse and test the JS doc signatures? Can I export comments? Will my tests fail if the types mismatch?
You can import types now, at least with VS Code.
``` /* * @typedef {import('./types.js').MyType} MyType */ ```
Haven't found myself using it, though.
When using TypeScript to avoid using TypeScript, yeah. It's not valid JSDoc: https://github.com/jsdoc/jsdoc/issues/1645
In practice, here are a couple of examples:
A thread in relation to "replace `fgrep` with `grep -F`": https://news.ycombinator.com/item?id=33189503
How people feel about Python's handling of Python 2 vs Python 3: https://news.ycombinator.com/item?id=34227760
I imagine there are a lot of edge cases to wade through before mainlining.
Kotlin and typescript are actually similar enough that transitioning from one to the other isn't that big of a deal. The key difference is that typescript tries to maintain compatibility with javascript whereas it's just a compilation target for kotlin-js.
Like with typescript, you use type safe wrappers for any javascript code you need to access. You can actually reuse typescript type definition files and generate Kotlin ones from them. Not perfect, sadly, but it works for simple libraries and you can manually deal with the more complicated ones. I use things like fluent-js, maplibre, and a few other libraries. I don't use react, but a lot of kotlin-js developers do. There are no real limitations on what you can use here.
The one compromise kotlin-js makes to enable interfacing with javascript code is the dynamic keyword, which you use to do the bait and switch style APIs you see a lot in the javscript world. It could be a list, a number, or a string, etc. Dynamic allows you to work with such APIs. It's a necessary evil. But otherwise, it's all good. It's strict by default. It doesn't have a mode where it is not strict. And this is what enables IDEs like intellij to be a lot smarter with Kotlin than it is with typescript.
If you ignore the few syntax and language features that are unique to either Kotlin or Typescript, the key difference is that typescript is more sloppy and it's mostly because of js compatibility. And since kotlin-js has almost none of that and can manage fine without it, it kind of proves that this level of sloppiness is simply unnecessary and redundant. A whole lot of downsides and not a lot of upsides. Typescript is much more sloppy than it needs to be. Making it less sloppy is the obvious way to improve it. It's why I use kotlin-js instead.
Using typescript we're already paying the costs of friction & indirection in tooling and learning another language. I know typescript has a ton of momentum and is unstoppable at this point but I wish things had turned out differently.
If you have a function `widgetifyFoo(object: Foo)` and some JSON that you're pretty sure returns a `Foo`, you'd like to be able to do `widgetifyFoo(JSON.parse(data))`. With `any` the method call should just work, but with `unknown` you need to be explicit in what kind of data you're expecting (`widgetifyFoo(JSON.parse(data) as Foo)`). That may be too verbose for some.
The `[1, 2, undefined].filter(x => !!x)` typing is just a limitation of the transpiler. The end result will be an array containing two numbers, but you need to do some complicated validation to get the type right; `filter`s can be very complex if you want them to be, and they do by contract return a (sub)selection of the input. The type guarantee you want as a developer is based on the implementation rather than the underlying type system.
I suspect the `Array.includes` implementation to be a choice by the devs. The check will only succeed if the type matches, so you have a choice between setting the right type on your array (`"matt"|"sofia"|"waqas"|"bryan"` if you want to check for `"bryan"`) or using the type error you receive as an indication your check makes no sense. If you have an immutable array of specifically `["matt","sofia"]`, why even check for `"bryan"`? It won't be there, unless you make a mistake!
Javascript will obviously happily call the method for you and do what you expect, so TypeScript is breaking valid JavaScript standard API code here. That said, I agree with the TypeScript devs that they made the right choice on that one.
That logic only applies to searching for a constant, and in that case it should go a step further and complain that you're using .includes at all. There's no reason to search for the constant "bryan" or the constant "matt".
The only time it makes sense to use .includes on this array is with a string of [semi-]unknown contents. And yet that's what gets blocked. The typing is wrong.
I could at least understand the use case for a narrow includes if you explicitly typed the array as `("matt"|"sofia"|"waqas")[]`. But that's not the type of the array. The type is `readonly ["matt", "sofia", "waqas"]`. There's always exactly one of each. There is never a reason to feed a string of type `"matt"|"sofia"|"waqas"` into includes. The only sensible parameters for includes are types that intersect with `"matt"|"sofia"|"waqas"` but can be other things too. Which means the most sensible parameter type to derive is `string`.
[1, 2, undefined].filter(Boolean). // number[]
is interesting because it makes use of the fact that the types of individual array members are known at compile time. I can think of some times when I have dealt with arrays that are defined at compile time, but not where I've needed to call .filter on such an array. Is there a common use case here?
That's very appealing to be; I commonly .map() an array to a nullable type, then want to filter out the nulls.
You'll often want to work on only the A's in the array, and so you need to whittle down the data, but also inform TypeScript that you've done so. The return type on your .filter() callback tell TS what type the entire resulting array should be.
When filtering out nulls, you normally have to write a user-defined type guard everywhere you call .filter(), which is irksome, so this project configures the built-in Boolean() function to behave similarly when passed to .filter().
If you treat `unknown` as C's `void*`, then you're not getting much out of your type checks.
Personally though, I think vscode should have a feature to automatically highlight `any` variables is TS code, which would serve roughly the same purpose.
As another commenter mentioned, this is much better when using something like Zod to do the cast safely.
`any` is a contagious/viral construct where anything it touches could become `any`. And then you spiral out of control and the whole codebase gets nothing from typescript but gets all the operational overhead.
If your browser doesn't support text fragments, can cmd/ctrl+f for "contagious" and/or "gradual" https://www.typescriptlang.org/docs/handbook/typescript-in-5......
Final note is that you likely shouldn't blindly cast unknown to stuff that you hope it is but instead use type guards to find out what the type is.
Zod is really good at making this ergonomic for most use cases. More niche types using nonprimitives or uncopyable attributes require hand rolled type guards :/
Type guards also update the type of your unknown var after it passes through them if you gate scopes with them if(isX(y)) {...}
Additional guards will merge attributes onto the type