With the ‘unknown’ type available is there a good case for ‘any’ anymore?
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.
This is a good thing for serious, long lasting projects. However, it could be an overkill for something fast and dirty.
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.
This is a bit of a problem imo. It’s super easy to forget to add a type somewhere and then it just defaults to any. I’d prefer no implicit any so you have to knowingly set something as untyped.
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".
Yup. And strict mode tends to guide devs towards better code/API design. Stuff like function/method overloads with dynamic argument types might seem convenient when the types are loosey goosey, but it usually becomes obvious they’re probably a bad idea when you have to specify the types upfront.
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.
> 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.
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.
‘any’ is great for porting javascript into typescript, and integrating with javascript libraries. You can add types gradually, without everything grinding to a halt.
That sounds extremely cumbersome. Sometimes I just want to throw an any on something and call a method on it without having to deal with types. How would unknown help with that? I can't call a method on a variable with unknown type.
You can use ‘as’ to cast unknown to any type. This is extra typing but it does signify to other developers that you’re calling a function on an unverified object, which I believe makes the code more maintainable in the long run.
> With the ‘unknown’ type available is there a good case for ‘any’ anymore?
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.
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 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)?
Thank you. That gives me a red underline under ContactId and Email now. I think because input.Contact__c and input.ContactEmail__c are unknown. I think this is the right direction. What is my next step?
(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'
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)
Nice I tbink that is the answer! It is for when the way you need to deal with the data is so dynamic and runtime specified that crafting out the interface/types to cast to would be painful to impossible. Data wrangling.
It's can be useful when you're integrating with 3rd party that doesn't support typescript and you just want to say "I don't care about types here". Of course you have to be careful as `any` turns off typing everywhere it appears. But if the usage is local within a function then this may not matter too much.
For one, it signals different intent, which tends to be important in larger projects with more collaborators. The more you can make intentions obvious, the easier it'll be for others to understand and contribute.
A value whose type we don't know is different from a value which we know could be any type.
Consider a ‘dump’ function. By design, it handles anything you throw at it - strings, objects, booleans, etc. It won’t change. That’s ‘Any’.
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.
The difference comes when you interface with other well-typed code and want to convert from any/unknown down to a specific type.
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-typing
A dump function accepting "unknown" is signalling "I'm not sure what this function could handle, probably a lot of different types" vs accepting "any" where it's telling you "I can accept any type you throw at me".
They're the same as much as "1 + 1 = 2" and "2 + 0 = 2" is the same. The result might be the same ("any" and "unknown" accepting any type) but again, the intent is different (a value we know can be anything vs a value we don't know what type it'll have).
It's a weird choice that this library makes some JS APIs more strict, and others less strict. Specifically, the changes to JSON/Array.isArray make sense, the value is unknown, and coercing it to any encourages leaking that type further, or runtime errors. However, the changes to includes/Set basically amount to "I said that this array contained this enum, but I changed my mind when I wrote this if statement". This actually allows more bugs to slip through, rather than fewer.
IIRC, in the past .json on fetch was taking the data type as a generic parameter. Then suddenly it got changed where the generic was removed and you need to cast it yourself. I wonder why that was done.
As a side note, in editors using js/ts language server from VSCode, you can benefit from these types even in "vanilla" JavaScript with JSDoc. It could look like:
// @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, expected
I really can’t stand the appeal. It’s so much more verbose and clunky than Typescript. I want to understand why people choose it but I can’t justify using a worse, more verbose, less readable, and less powerful version of Typescript when I can just change a j to a t and have the real deal.
Not that I've implied it is better or worse or demanded subjective opinions about that, but when we are at it, here are some remarks:
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.
> Knowing that what is shipped is what you wrote, not what some transpiler spat out, might have some value.
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?
I'm working on a JS project for a client. I can't modify the deployment procedure so I can't add a build step and ts-node is no help because it's frontend.
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.
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 still use `tsc` to validate the jsdoc types. It will spit out errors when types don’t match.
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.
I won’t speak for others, but I for one can’t stand the amount of extra packages needed to get a TypeScript project working. I need to install adapters for my linter, formatter, test runner, editor, bundler to name a few. Hopefully it all works together with all the other plugins and adapters. With the jsdoc version, I just install the `typescript` package, and I can use that to do the typechecking as well export types to be used by a TypeScript project. Sure it’s a bit more verbose, but I’d take that over tinkering with dependencies and configurations.
That's the point. Suggested setup (editor with JS/TS language server constantly checking your code) means you will know about every problem that would possibly break something, you just don't need separate "source code" producing "build package". It means you can ship your code directly to target platform that only understands JavaScript.
AFAIK that's not what you currently can do with TypeScript (except Deno).
I've been using kotlin-js for the past few years. Not an obvious choice for a lot of typescript users but IMHO something that would lead to a lot of obvious improvements to typescript if more people would. Like being more strict by default and getting rid of a lot of the javascript compatibility. This library seems like a noble attempt to patch it up somewhat. But why not go all the way and just fix the language properly?
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.
This is basically how I feel about rescript these days. Typescript is really amazingly complex for what it ends up giving you. It's a big improvement over js, but I don't think that's the right comparison.
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.
I think TypeScript's decisions about these methods were done either because early versions of the transpiler couldn't detect certain behaviour or to make the language friendlier to work with.
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.
> 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!
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.
If you typed the array as string[], it would generally work, but then the compiler wouldn't be able to warn you that users.includes("matt") is always true. And it wouldn't be able to warn you that users.includes("bryan") is always false.
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`.
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?
I think that's just a contrived example. It looks like it should work with types like `(string | null)[]`, where individual members are only known on runtime.
That's very appealing to be; I commonly .map() an array to a nullable type, then want to filter out the nulls.
Yeah, it only exists at compile time, so in "real" code you won't know the type of e.g., array index 2 at compile-time, but you will know "each item in this array is either type A or type B".
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().
Reading https://github.com/total-typescript/ts-reset/blob/main/src/e..., I'm more confused than before. The generic NonFalsy<T>[] looks to me like it should evaluate to never[] if the entire array is false, and to T[] (the original array type, not narrowed) otherwise. But I can see that the test case demonstrates that it works. What am I missing?
I’m not sure anybody has ever convinced me that unknown is meaningfully better than any, in any scenario (pun). I get that it’s “typesafe” on the surface but we both know you’re going to turn around and cast it to the type you think it is so what’s the point
You can safely cast an `unknown` to a desired type with a runtime check from a library like Zod (https://zod.dev/). The `unknown` makes sure you don’t forget a check.
One advantage is that unknown makes it clear at a glance where the unsafe cast is happening, and forces devs to do it in a reasonable place (rather than letting `any` propagate through the code accidentally).
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.
It depends on your setup. I've worked with a tslint setup that treats `any` as an error, only accepting `unknown` (or in bad situations, `never`) for stuff like Javascript code.
If you treat `unknown` as C's `void*`, then you're not getting much out of your type checks.
In that case what's the point of any type system at all? Any type can be cast to another. If you don't just arbitrarily cast whenever you like, unknown is extremely useful to remind you to type check or parse before you use a value.
If you have tight interfaces that fully encapsulate the use of any, you're fine. If you want to return `any` or pass it around outside of an insulated zone, you're gonna have a bad time.
`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.
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
`any` is infectious, a blackhole for type safety. You can accidentally pass values with `any` all over the place and TS won't complain, but it also won't catch any mistakes you make. `unknown` is much more restrictive and you can't accidentally pass those values around without refining the type, it's much safer.
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.