Skip to content

Latest commit

 

History

History
245 lines (179 loc) · 9.13 KB

File metadata and controls

245 lines (179 loc) · 9.13 KB

Items 42-46

Item 42: Use unknown Instead of any for Values with an Unknown Type

Suppose you’re writing a YAML parser. It’s tempting to use any for result of parse function

function parseYAML(yaml: string): any { ... }

But this directly goes against Item 38’s advice to avoid returning any types from functions. This will cause any variable without type declarations to be quietly (implicitly) inferred as an any type.

const book = parseYAML(`
  name: Jane Eyre
  author: Charlotte Brontë
`)
alert(book.title) // no error, alerts undefined
book('read') // no error but runtime error 

A safer alternative is to return an unknown type

function safeParseYAML(yaml: string): unknown { return parseYAML(yaml) }
const book = safeParseYAML(`
	name: Jane Eyre
  author: Charlotte Brontë
`)
alert(book.title) // Object is of type 'unknown'
book("read") // Object is of type 'unknown'

any's assignability:

  • Any type is assignable to the any type
  • The any type is assignable to any other type (except for never)

In context of Item 7, any type is simultaneously both a subset and a superset of all other sets. Because type checker is set-based, the use of any effectively disables it.

unknown's assignability:

  • Any type is assignable to the unknown type
  • unknown is only assignable to unknown or any

💡You can’t really do much with unknown, and that encourages you to assign appropriate types when you do want to use the value. Since unknown is not assignable to types other than unknown or any, type assertion is required. But this is an appropriate usage of type assertion because we really do know more about the type of the value than TypeScript does.

const book = safeParseXML(`
	name: Jane Eyre
  author: Charlotte Brontë
`) as Book
alert(book.title) // Property 'title' does not exist on type 'Book'
book('read') // this expression is not callable

You can also recover a type from an unknown object by using:

  • an instanceof checks
  • a user-defined type guard (with a few more hoops than usual)
function processVal(val: unknown) {
	if (val instanceof Date) {
		val // Type is Date
	}
	if (isBook(val)) {
		val // Type is Book
	}
}

function isBook(val: unknown): val is Book {
	return (
		typeof(val) === 'object' && val !== null && // needed to avoid errors on the `in` checks
		'name' in val && 'author' in val
	)
}

You can also use a generic parameter instead of unknown, but this is considered a “bad style” in TypeScript. Better to just return unknown and force your users to use an assertion or narrow th the type they want.

function safeParseYAML<T>(yaml: string): T { ... }

You can also use unkown in “double assertions”

declare const foo: Foo
let barAny = foo as any as Bar
let barUnk = bar as unknown as Bar // safer in the future

Two other types similar to unknown:

  • The {} type: consists of all values except null and undefined
  • The object type: consists of all non-primitive types

Item 43: Prefer Type-Safe Approaches to Monkey Patching

Monkey Patching:

window.monkey = 'Tamarin'
document.monkey = 'Howler'

const el = document.getElementById('colobus')
el.home = 'tree'

RegExp.prototype.monkey = 'Capuchin'
/123/.monkey // 'Capuchin'

Possible because you can add arbitrary properties to JavaScript’s objects and classes.

These approaches are generally not good designs:

  • When you do it on window or document, you’re essentially adding a global variable
  • With TypeScript, type checker only knows about built-in properties of those objects and will throw errors for properties you added. (can be fixed with an any assertions)
(document as any).monkey = 'Tamarin'

There are a few solutions that are better than using any assertions to fix type errors for monkey patching:

  • Best solution is to keep your data out of document or the DOM. (Avoiding monkey patching)
  • Another solution is to use interface augmentations
// This will be public to other parts of your code or other libraries
interface Document {
	monkey: string | undefined // because you can't always gaurantee that this property is assigned before being used
}
// in modules
declare global {
	interface Document {
		monkey: string | undefined
	}
}

document.monkey = 'Tamarin'
  • Another approach is to use a more precise type assertion
interface MonkeyDocument extends Document {
	/** Genus or species of monkey patch */
	monkey: string
}

(document as MonkeyDocument).monkey = 'Macaque'

Item 44: Track Your Type Coverage to Prevent Regressions in Type Safety

Even with noImplicitAny enabled, any types can still enter your program in two main ways:

  • Explicit any types
function getColumnInfo(name: string): any {
	return utils.buildColumnInfo(appState.dataSchema, name) // Returns any
}
  • From third-party type declarations
declare module 'my-module'
import {someMethod, someSymbol} from 'my-module'
const pt1 = { x: 0, y: 1 }
const pt2 = someMethod(pt1, someSymbol) // pt2 is any

There are several reasons why these any types were introduced.

  • Maybe there was a bug in type declarations
  • Maybe the type declaration for a return type was not specific enough, and you didn’t want to deal with it immediately
  • Maybe the third party library you were using was not typed yet (similar to example above)
  • etc.

However, these reasons might not apply any more:

  • Maybe there’s a type you can plug in now where you previously used any
  • Maybe an unsafe type assertion is no longer necessary
  • Maybe the bug in the type declarations has been fixed

You might want to keep track of these any types and revisit decisions you made about using any types in order to inrease type safety over time

Item 45: Put TypeScript and @types in devDependencies

Three types of dependencies in npm:

  • dependencies: Packages that are required to run your JavaScript. When you publish your code on npm and another user installs it, it will also install these dependencies.
  • devDependencies: Packages that are used to develop and test your code but are not required at runtime.
  • peerDependencies: Packages that you require at runtime but don’t want to be responsible for tracking.

TypeScript belongs in devDependencies:

  • Globally installing TypeScript is not recommended
  • TypeScript is not needed in runtime (types do not exist in runtime)
  • Your IDE will easily discover a version of TypeScript installed via devDependencies

@types dependencies (DefinitelyTyped) also belong in devDependencies:

  • You should publish JavaScript, not TypeScript, and your JavaScript does not depend on @types when you run it

Item 46: Understand the Three Versions Involved in Type Declarations

There are three versions you must track when you are using other JavaScript libraries

  • The version of the package
  • The version of its type declarations (@types)
  • The version of TypeScript

There are a few ways this can go wrong:

  • You might update your library but forget to update its type declarations.
    • Update your type declarations so they’re back in sync
    • If the type declarations have not been updated, you can use an augmentation in your project, or you can contribute updated type declarations back to the community
  • Your type declaration might get ahead of your library
    • Upgrade your library or downgrade the type declaration
  • Type declarations might require a newer version of TypeScript than you’re using in your project
    • This will manifest as type errors in @types
    • Either upgrade your TypeScript version or use an older version of type declarations
  • You can wind up with duplicate @types dependencies
    • Happens if two different libraries have common transitive @types dependencies at different versions
    • Update your dependencies so that they’re compatible

Some packages, particularly those written in TypeScript choose to bundle their own type declarations. This solves problem of version mismatch, but this can still cause other problems:

  • What if there’s an error in the bundled types that can’t be fixed through augmentation?
  • What if the bundled types don’t work with newer version of TypeScript?
    • This can easily keep you stuck on an old version of TypeScript
    • DefinitelyTyped can have much quicker response to breaking changes because Microsoft runs TypeScript against all the type declarations on DefinitelyTyped when they develop it
  • What if your types depend on another library’s type declarations?
    • This is a problem because you want to keep @types in devDependencies, and the users of your library will not get your devDependencies
    • If you publish your types on DefinitelyTyped, this won’t be a problem
  • What if you need to fix an issue with the type declarations of an old version of your library?
  • How committed to accepting patches for type declarations are you?
    • Can you commit to a turnaround time similar to that of DefinitelyTyped?

If you’re publishing packages, understand pros and cons of bundling types versus publishing them on DefinitelyTyped. Prefer bundling types if your library is written in TypeScript and DefinitelyTyped if it is not.