Typescript: variable type resests to "unknown" even after validation

Viewed 21

I have the following code snippet:

See it in the Typescript Playground.

// this class is imported by the validator.ts module
class EWC extends Error {
    constructor(public message: string, public code: string) {
        super(message);
    }
}

// The assert function is coming from an external library
// and it has the following signature    
interface AssertOtherArg {
    name: string;
    code: string;
}

function assert<Type>(schema: string, value: unknown, other: AssertOtherArg): asserts value is Type {
    throw "unimplemented";
}
// end of assert


// ------- [string-validator.ts] ----------------------------------------------

// import {assert} from "some-lib";
// import EWC from "../util/ewc"

interface OtherInfo {
    name: string;
    typeErrorCode?: string; 

    trimBeforeLenghtValidation?: boolean;

    minLength?: number;
    minLengthErrorCode?: string;

    maxLength?: number;
    maxLengthErrorCode?: string;
}

function assertValidString(str: unknown, otherInfo: OtherInfo): asserts str is string {
    const {name, trimBeforeLenghtValidation = false} = otherInfo;

    {
        const code = "typeErrorCode" in otherInfo ? otherInfo.typeErrorCode : `invalid_${name.toLocaleUpperCase()}`;
        assert<string>("string", str, {name, code});
    }

    str; // here str: string

    if(trimBeforeLenghtValidation) str = str.trim();

    str; // here str: unknown! why?

    // min length vlaidation
    if("minLength" in otherInfo) {
        const code = "minLengthErrorCode" in otherInfo ? otherInfo.minLengthErrorCode : `${name.toUpperCase()}_TOO_SHORT`;

        const {minLength} = otherInfo;

        // the problem is happening here
        // Error: Object is of type 'unknown'.(2571)
        if(str.length < minLength)
            throw new EWC(`"${name}" cannot be shorter than ${minLength} character(s) long!`, code);
    }

    // maxLength validation
    // ...
}

In the assertValidString function, I'm doing an assertion that the argument str is of type string. And you can verify that after the assertion the type of str is indeed string.

assert<string>("string", str, {name, code});

str; // str: string

But the problem occurs after this line:

if(trimBeforeLenghtValidation) str = str.trim();

Now the type of str argument is unknown and it's causing problems further down the line. For example: when I try to access the length property:

// Error: Object is of type 'unknown'.(2571)
if(str.length < minLength)
    throw new EWC(`"${name}" cannot be shorter than ${minLength} character(s) long!`, code);

I get the error:

Object is of type 'unknown'.(2571)

I don't understand why this is happening and it seems really weird to me. I know that I can just apply the any hack as str: any in the function parameter declaration but I really want to understand why this is happening! Please enlighten me about this topic with your Typescript knowledge.

Sorry for my bad English. Thanks in advance <3.

1 Answers

This is a bit technical, but in a nut shell when you assign to a variable that has the initial type of unknown it will widen the type to unknown.

It doesn't matter if TS could infer the type.

You'll need to do another assertion after doing str = str.trim().

The problem stems from how TS widens types after an assignment and then narrows it back down. In your case after the assignment the potential type is unknown | string, but when having unknown in an union the final type becomes unknown (a union type with any being the only exception).

Related