Skip to content

Allow more constructs to work as type guards for unknown #25720

Description

@AlCalzone

Search Terms

unknown type guard
Related: #24439 (comment), #25172

Suggestion

Currently, only a very limited set of type guards are able to narrow the new unknown type:

  • Array.isArray (because it is a typeguard for arg is any[]) and probably some more in the lib files
  • instanceof
  • self-written typeguards

However to make working with unknown types less awkward, I'd like to see a couple of other constructs being able to narrow the unknown type:

let x: unknown;
// Direct equality should narrow to the type we compare to
x === "1"; // should narrow x to string or the literal "1" type, similar for other types aswell

// All these should narrow x to {prop: any}
"prop" in x;
x.prop != null;
x.prop !== undefined;
typeof x.prop !== "undefined";

// typeof should work on properties of the unknown variable
typeof x.prop === "string"; // should narrow x to {prop: string}

Use Cases

Make unknown easier to work with!

// before, very verbose!
const x: unknown = undefined!;

function hasProp1(x: any): x is {prop1: any} {
	return "prop1" in x;
}
function hasProp2(x: any): x is {prop2: any} {
	return "prop2" in x;
}
// imagine doing this for many more properties

if (hasProp1(x)) {
	x.prop1;
	if (hasProp2(x)) {
		x.prop2;
	}
}

// ===========

// after, much more concise and less overhead
const x: unknown = undefined!;
if ("prop1" in x) {
	x.prop1;
	if ("prop2" in x) {
		x.prop2;
	}
}

Checklist

My suggestion meets these guidelines:

  • This wouldn't be a breaking change in existing TypeScript / JavaScript code
  • This wouldn't change the runtime behavior of existing JavaScript code
  • This could be implemented without emitting different JS based on the types of the expressions
  • This isn't a runtime feature (e.g. new expression-level syntax)

Activity

  1. mattmccutchen commented on Jul 17, 2018

    @mattmccutchen
    Contributor

    The use of in is covered by #21732. Shall we add the other checks for a property (comparison to undefined, typeof) to #21732 and call this a duplicate of #21732 + #25172?

  2. AlCalzone commented on Jul 17, 2018

    @AlCalzone
    ContributorAuthor

    Sure, why not!

  3. AlCalzone commented on Jul 19, 2018

    @AlCalzone
    ContributorAuthor

    And one more thing:

    let foo: unknown;
    if (typeof foo === "object") {
    	// foo should probably be narrowed to {[prop: string]: unknown} here
    }
  4. mhegazy commented on Jul 27, 2018

    @mhegazy
    Contributor

    Similar requests in #10715, #25172, and #21732

  5. simonbuchan commented on Aug 21, 2018

    @simonbuchan

    I was hoping unknown would let me have type-safe data-loading, e.g.:

    interface SomeValue { a: string, b?: number, c: 'left' | 'right' }
    function readValue(id: string): SomeValue {
      const u: unknown = await someDataSource(id);
      if (
        typeof u !== 'object' ||
        u === null ||
        typeof u.a !== 'string' ||
        b in u && typeof u.b !== 'number' ||
        u.c !== 'left' && u.c !== 'right'
      ) {
        throw new Error(util.format('Invalid value with id %O from some data source: %O', id, u));
      }
    
      return value; // TS is checking that the checks above actually verify the return type here
    }

    This to me would be a better match to TS for what #26078 wants, but I wouldn't complain about adding quick-fixes to add the missing checks!

    (remember that typeof u === "object" should actually narrow to object | null - Thanks javascript!)

  6. w0rp commented on Sep 12, 2018

    @w0rp

    I would like it if type guards with unknown worked a little more like this.

    let x: unknown
    
    if (typeof x === 'object' && x !== null && 'foo' in x && typeof x.foo === 'string') {
      /* x is promoted to {foo: string} here */
    }
    

    I think the type promotion ought to work like so, if at all possible.

    1. typeof unknown === 'object' -> object | null
    2. (object | null) !== null -> object
    3. 'foo' in object -> {foo: unknown}
    4. typeof {foo: unknown}.foo === 'string' -> {foo: string}
  7. anup-the-magic commented on Dec 5, 2018

    @anup-the-magic

    I realize I'm a bit late, but you might be interested in https://github.com/gcanti/io-ts -- provides a nice way to generate your necessary typeguards, though might be a bit heavy handed for the common usecase (and thus probably still worth considering this issue)

  8. w0rp commented on Dec 5, 2018

    @w0rp

    Thanks for the suggestion, but that's probably not relevant to the discussion.

  9. talbenari1 commented on Mar 20, 2019

    @talbenari1

    I'd also like to add that unknownValue instanceof Array should really be refined to unknown[], not any[] as is the current behavior. I'm also not getting warnings about implicit any when I do that.

  10. AndreasGassmann commented on Aug 2, 2019

    @AndreasGassmann

    Is there any update regarding this issue? I would love to use the unknown type, but at this point it's just too verbose to narrow it down to bigger objects. This proposal would make it a lot easier.

  11. butchler commented on Sep 20, 2019

    @butchler

    Until this is fixed, this is a helper that can be used to make it easier to write manual type guards for unknown types that you expect to be nested objects:

    export function isUnknownObject(x: unknown): x is { [key in PropertyKey]: unknown } {
      return x !== null && typeof x === 'object';
    }
    

    Example usage:

    function example(x: unknown) {
        if (isUnknownObject(x) && isUnknownObject(x.prop) && typeof x.prop.subProp === 'string') {
            console.log(x.prop.subProp);
        } else {
            console.log('Could not find subProp');
        }
    }
    
    example({
        prop: {
            subProp: 'test',
        }
    });
    example({});
    

    For more complicated use cases, using something like https://github.com/gcanti/io-ts is probably a better option than writing the type checks manually, but isUnknownObject can be useful for simple cases.

  12. stephenlautier commented on Oct 28, 2020

    @stephenlautier

    Adam Buechler (@butchler) Similar to what you suggested (infect i started with that)

    function isAssumedType<T = Record<string, unknown>>(x: unknown): x is Partial<T> {
    	return x !== null && typeof x === "object";
    }
    
    // usage
    if (isAssumedType<CommandCreator>(arg) && arg.execute && arg.host) {
        return true;
    }

    The main difference is that arg will be partially typed so when you do checks with props they are bound to the interface so you can F2 rename safely and will also be updated

  13. butchler commented on Oct 29, 2020

    @butchler

    isAssumedType<CommandCreator>(arg)

    This is effectively the same as a type assertion (i.e. arg as Partial<CommandCreator>), but unlike a type assertion it does not use an explicit as keyword and it implicitly changes the type of arg in the following expressions.

    Type assertions are fine and have to be used sometimes, but personally I would avoid using something like isAssumedType because 1) it is less explicit so other people reading the code might not realize a type assertion is being made and 2) it makes it very easy and convenient to use type assertions, which is probably a bad thing because type assertions should generally be avoided when possible.

  14. stephenlautier commented on Oct 29, 2020

    @stephenlautier

    Yes, naming is not the best I agree (but whatever, you can call it as you want), and to be honest I only use it private in file along with some type guards so my main intention is usage in guards so far.

    As you suggested arg as Partial<CommandCreator> doesnt work inlined within the condition e.g.

    image

    Whereas with my suggestion works as following:
    image

    So again, the main benefit is that F2 rename works (which is a huge plus imo)
    Anyway just wanted to share.

  15. username-not-known commented on Jan 26, 2021

    @username-not-known

    if (typeof x === 'object' && x !== null && 'foo' in x && typeof x.foo === 'string') {
    /* x is promoted to {foo: string} here */
    }

    typeof x.foo doesn't work -> Property 'foo' does not exist on type 'object'.

  16. w0rp commented on Jan 26, 2021

    @w0rp

    if (typeof x === 'object' && x !== null && 'foo' in x && typeof x.foo === 'string') {
    /* x is promoted to {foo: string} here */
    }

    typeof x.foo doesn't work -> Property 'foo' does not exist on type 'object'.

    That's an example of something I would like to work, but currently doesn't.

  17. username-not-known commented on Jan 26, 2021

    @username-not-known

    if (typeof x === 'object' && x !== null && 'foo' in x && typeof x.foo === 'string') {
    /* x is promoted to {foo: string} here */
    }

    typeof x.foo doesn't work -> Property 'foo' does not exist on type 'object'.

    That's an example of something I would like to work, but currently doesn't.

    Maybe something like this works.. It does the job for me.. Here 'result' is of type unknown. So narrowing down to the required type using user defined type guard seems to be the sane type-safe solution. Or are there are other possibilities as well?

    CaptureUDTG

  18. tushar-sg commented on Jul 14, 2021

    @tushar-sg

    Would like to see this implemented.

    function isTest(arg: unknown): arg is Test {
      return (typeof arg === "object") && (arg !== null) &&
        ("quest" in arg) && (typeof arg.quest === "string");
    }
    
    interface Test {
      quest: string;
    }
    

    Typescript still complains for the above code even though we check for all the cases before accessing the quest property.

    Using any instead of unknown takes away the benefits of using TS in the first place.

  19. rattrayalex commented on Sep 8, 2021

    @rattrayalex

    I would expect ?. to work as well with an unknown err, though perhaps there's a case I haven't thought of which would cause failure, eg:

    if (err?.message) {
      // err is typed as { message: unknown, [k: string]: unknown }, or similar. 
    }
  20. 1valdis commented on Dec 3, 2021

    @1valdis

    The lack of better/easier type guards for unknown type is why I'm not using it in try...catch blocks as well. I should be able to do simply

    catch (e: unknown) {
      console.log(e?.message);
    }

    I don't want to overcomplicate my code just because unknown is so hard to work with so I'm sticking to any, yet I'd happily use unknown if the code above and in the other proposed examples worked.

  21. jcalz commented on Sep 6, 2022

    @jcalz
    Contributor

    Crosslinking to #27706

  22. jguddas commented on Jan 19, 2023

    @jguddas

    Adding this kind of type of guards to Arrays, Maps and Sets could be done quite easily, Objects are be a bit more complicated.

    const inp = process.env.INPUT as unknown
    
    const arr = ['a', 'b', 'c'] as const
    if (arr.includes(inp)) {
      console.log(inp)
      //          ^? "a" | "b" | "c"
    }
    
    const obj = {'a':1, 'b':2, 'c':3} as const
    if (obj.hasOwnProperty(inp)) {
      console.log(inp)
      //          ^? "a" | "b" | "c"
    }
    
    const map = new Map([['a',1], ['b', 2], ['c', 3]] as const)
    if (map.has(inp)) {
      console.log(inp)
      //          ^? "a" | "b" | "c"
    }
    
    const set = new Set(['a', 'b', 'c'] as const)
    if (set.has(inp)) {
      console.log(inp)
      //          ^? "a" | "b" | "c"
    }
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    Awaiting More FeedbackThis means we'd like to hear from more people who would be helped by this featureSuggestionAn idea for TypeScript

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions