Type Driven Development: unknown vs any

Written by

in

Part 6 – unknown vs any

This is a series of posts I’m writing about using types as another tool in software development, Continuous Delivery, & keeping LLM’s honest. They’re also a design & refactoring tool, a communication tool, and reduce how many tests you have to write.

While good type systems give you confidence “when it compiles, it works”, and enables fearless refactoring, we’re often getting data _into_ our type system that isn’t typed.

whatIsThisWhoKnows = await response.json()Code language: JavaScript (javascript)

How you handle this depends on the type system, but for Gradually Typed ones like Luau, Python, and TypeScript, you have 2 options with various trade offs: any and unknown (Python calls it Any / object).

const person = JSON.parse(data) as any
// ⚠️ Compiler allows this dangerous code 
// because person's type is any
console.log(person.name)Code language: JavaScript (javascript)

Any turns the type system off. This sounds horrifying at first, but it’s also why gradual type systems are so popular; they enable you to git-r-done, you can integrate w/untyped code namely libraries/modules you didn’t write, or when dealing w/3rd party data that you aren’t really sure what the type is.

const figureOutWhatThisIs:any = JWT.validate(jsonWebToken) as any
console.log("figureOutWhatThisIs:", figureOutWhatThisIs)Code language: JavaScript (javascript)

Despite types software has a rule of “we have to run it to know what it does” and even in something type-powerful like Elm/Scala/Haskell, sometimes we need to run the code to “see” or log the data, especially from some 3rd party / code that isn’t ours. I can’t count how many times a REST API said it delivered one thing, but ended up delivering something either slightly or completely different. Other times you just set your types up wrong, and need to test ’em at runtime.

That said, if you want to be type-safe, and practice Type Driven Development, you should avoid any. Any turns off the type system, so all the work you did on creating types can be negated by one mis-typed any slipping in. Instead, we should be using types to _narrow_ what we want and instead using the unknown type helps us do that.

const person = JSON.parse(data) as unknown
// ✅ Compiler does NOT allow this, 
// have to type narrow first
console.log(person.name)Code language: JavaScript (javascript)

For now, recognize any is a tool to explicitly turn off the TypeScript compiler. If you can, try to use unknown instead, and type narrow to what you _think_ it should be. If it fails at runtime, type narrow to that, then try again. This ensures data coming from outside must be type narrowed first, and you can add extra unit & fuzz/property tests on this part as well to help.

if(personMaybe
  && typeof personMaybe === 'object'
  && 'name' in personMaybe
  && typeof personMaybe?.name === 'string') {
    // then we're probably safe here 
    // to cast as a Person
    const person = personMaybe as PersonCode language: JavaScript (javascript)

However, while type narrowing to help the compiler is powerful, it’s also fraught with foot guns. We’ll see how we fix that with schemas.

Comments

Leave a Reply

Your email address will not be published. Required fields are marked *