> 
blog_rick_love>  

Typescript 'any' is evil

titleTypescript 'any' is evildate2020-10-13path/typescript-any-is-evilauthorRick LoveexcerptIn Typescript any is eviltagstypescript, type-system, code-wisdom, tutorial

tl;dr: In Typescript, never use any

any is evil

any is typescript's rebelious 19 year old who was doing 80 on an icy mountain road, drove off a cliff, bounced down the side of the mountain in a giant fireball, was thrown from the fireball car, landed in a frozen river, and was finally swept out to sea and eaten by sharks.

any is the implementation in typescript of the 'this is fine' meme. This gif is perfect:

any => bugs

Why would you want any bugs in your code?

The type flow checker in typescript is your first line of defense against bugs. It is your most important tool to ensure correctness of your code.

any disables all type checking of any code that touches that variable.

That should sound bad.

It is.

What is the right way

  • Use the minimal type (the part of the type you actually care about) - Duck Typing.
  • Use a partial type.
  • Use union types when it could be multiple types. Discriminated unions are great in typescript.
  • Use unknown if you really have to. Then cast it to one of the above before you access it.

The Minimal Type - Duck Typing

Don't use the full type. Define only the fields you need to access in that scope.

For example, when you define a function, use a minimal definition for the argument types. Also, as a bonus that function is more generic and it is not coupled to a complex type. This simplifies your dependency graph and is one of the greatest advantages of Typescript over most other typed languanges: Typescript implements duck-typing.

// I only care about the id field
export const getKey = (item: { id: string }) => {
    return item.id;
};

// Got some junk here
const complexObject = {
    id: `ABC-012345`,
    got: () => `you`,
    too: `2` == 2 && 2 == true && `2` !== 2 && 2 !== true,
    much: () => { while (true) { complexObject.much(); } },
    stuff: [1, `42`, `false`, null, { undefined: true }],
    here: { latitude: 42, longitude: 42 },
};

// The complexity of the object doesn't matter
const key = getKey(complexObject);
console.log(`Here is the key`, { key, complexObject });

The Partial Type

In the case where an object should have a specific type, but it cannot be guaranteed, use partial typing.

Also in most cases, when the object fails to have the correct type, you can simply return undefined or null.

Below are a few patterns to do that depending on how you want to handle null and whether you want to support the whole parameter being optional.

I default to the first. If I discover that many callers would need to convert null to undefined, then I add | null.

// Basic optional field
export const getKeyWithOptionalId = (
    item: { id?: string },
) => {
    // Return type: string | undefined
    return item.id;
};

// Same as above using Partial Generic Type
export const getKeyWithPartialType = (
    item: Partial<{ id: string }>,
) => {
    // Return type: string | undefined
    return item.id;
};

// Support null or undefined field
// but return undefined instead of null
export const getKeyWithNullableId2 = (
    item: { id?: null | string },
) => {
    // Return type: string | undefined
    return item.id ?? undefined;
};

// Support null or undefined object
// but require the caller to provide something
export const getKeyWithNullableObject = (
    item: null | undefined | { id?: null | string },
) => {
    // Return type: string | undefined
    return item?.id ?? undefined;
};

// Support optional parameter
export const getKeyWithOptionalArgument = (
    item?: null | { id?: null | string },
) => {
    // Return type: string | undefined
    return item?.id ?? undefined;
};

Here is a more complex example:

export const getKeyFromObject = (
    obj?: {
        id?: string;
        uid?: string;
        guid?: string;
        uuid?: string;
        key?: string;
    }
) => {
    return obj?.id
        ?? obj?.uid
        ?? obj?.guid
        ?? obj?.uuid
        ?? obj?.key;
};
Side Note: null vs undefined

On the difference between undefined and null. They should be treated the same logically in your code.

However, I do sometimes use them to distinguish intent in the code:

  • undefined to indicate an empty optional field - i.e. it doesn't exist and that's normal
  • null to indicate the default value - i.e. an object that normally should have value, but is not initialized yet

However, those differences are subtle and it may be better to just use undefined for everything. Logically they should be treated the same in most cases.

In fact, the only place I have seen a logical difference between them is when updating an object in a storage system:

  • undefined could mean ignore the field (no change)
  • null could mean reset that field (remove the value)

However, that pattern is likely to introduce bugs so be careful. Thankfully, typescript keeps track of each and allows you to specify if one or the other is required.

Union Types

With union types, we are starting to use Typescript's real power.

A union type is essentially the or operator (|) for types: i.e. it's a type that could be any of multiple types.

Typescript handles union types like a pro. It understands all the runtime operations that could constrain a type and protects you from mistakes. For example, an if statement can be used to constrain the type to a specific possibility.

In the below example, keyOrObject could be a string, number, null/undefined, or an object with one of many possible id fields.

export const getStringKey = (
    keyOrObject?: null | string | number
        | { id?: null | string | number }
        | { uid?: null | string | number }
        | { guid?: null | string | number }
        | { uuid?: null | string | number }
        | { key?: null | string | number }
): undefined | string => {
    if (typeof keyOrObject === `string`) { return keyOrObject; }
    if (typeof keyOrObject === `number`) { return `${keyOrObject}`; }

    if (!keyOrObject) { return undefined; }
    if (`id` in keyOrObject) { return getStringKey(keyOrObject.id); }
    if (`uid` in keyOrObject) { return getStringKey(keyOrObject.uid); }
    if (`guid` in keyOrObject) { return getStringKey(keyOrObject.guid); }
    if (`uuid` in keyOrObject) { return getStringKey(keyOrObject.uuid); }
    if (`key` in keyOrObject) { return getStringKey(keyOrObject.key); }

    return undefined;
};

And here is the type when you hover over the object after being constrained by one of the if statements:

As you can see, inside the if body, the compiler know which specific type the keyOrObject must be.

Also, you can combine the union objects into a single partial object which can reduce the branching:

// Same as above
export const getStringKey = (
    keyOrObject?: null | string | number
        | {
            id?: null | string | number;
            uid?: null | string | number;
            guid?: null | string | number;
            uuid?: null | string | number;
            key?: null | string | number;
        }
): undefined | string => {
    if (typeof keyOrObject === `string`) { return keyOrObject; }
    if (typeof keyOrObject === `number`) { return `${keyOrObject}`; }

    return getStringKey(
        keyOrObject?.id
        ?? keyOrObject?.uid
        ?? keyOrObject?.guid
        ?? keyOrObject?.uuid
        ?? keyOrObject?.key
    );
};

Unknown

unknown is the little brother of any. Having grown up with the embodiment of a "Don't Do Drugs" campaign, unknown is not interested in the "freedom" of his evil brother.

unknown is safe. If you ask him to hold a baby, he won't drop kick it through a field goal or throw it into a lake.

(Bonus points if you have played Peasant's Quest - Note: the baby turns out ok.)

So when should you use unknown? Rarely - in fact it is difficult to come up with a good example where one of the above won't work much better.

Here is an example where it just doesn't matter what the type is because it is immedietely converted to json:

export async function webRequest(
    url: string,
    data: unknown,
    options?: { method: 'POST' | 'PUT' },
) {

    const body = JSON.stringify(data);
    const reqData = {
        method: options?.method ?? `POST`,
        headers: {
            'Accept': `application/json`,
            'Content-Type': `application/json`,
            'Content-Length': `${body.length}`,
        },
        body,
    };

    throw new Error(`Not Implmented`);
}

Other Articles about any

Fun Side Note: Insanely Precise Typing

Typescript is the most precisely typed languange that exists and it gets more powerful constantly.

Some people have taken that to the level of insane:

Let me explain what is happening in that screen shot:

Typescript (4.1) is parsing a raw string and determining:

  • An sql join calculation
  • The fields returned by that query
  • Dot operator object navigation of those fields
  • Specific result values (as string constants) when that query is applied against an object literal

Let me emphasize this. Typescript doesn't have a single line of code that knows anything about SQL. This demo was made just to demonstrate that typescript can rip apart raw string constants and understand type information from the contents of that string. This is done with a combination of typescript's conditional typing and it's support for string literals soon coming in Typescript 4.1.

Here is the project: https://github.com/codemix/ts-sql

Typescript Typing Level: Insane

Related Articles

2020-11-03Course: Todo App (Unfinished)typescript2020-10-30Emoji Variable Namestypescript2020-10-24Typescript Type System Adventuretypescript, type-system2020-10-13Typescript 'any' is eviltypescript, type-system, code-wisdom, tutorial2020-09-24User Beneficialcode-wisdom2020-05-12Customize Gatsby Blogtutorial2020-05-11Build Gatsby Blogtutorial

Comments

Root

© 2020 Rick Love