unknown type guard
Related: https://github.com/Microsoft/TypeScript/pull/24439#issuecomment-394185089, https://github.com/Microsoft/TypeScript/issues/25172
Currently, only a very limited set of type guards are able to narrow the new unknown
type:
arg is any[]
) and probably some more in the lib filesHowever 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}
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;
}
}
My suggestion meets these guidelines:
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?
Sure, why not!
And one more thing:
let foo: unknown;
if (typeof foo === "object") {
// foo should probably be narrowed to {[prop: string]: unknown} here
}
At the least, at typeof foo === "object"
it should narrow to object
. Currently remains unknown
and the following fails:
function f(u: unknown): object { return typeof u === "object" ? u : {}; }
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!)
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.
typeof unknown === 'object'
-> object | null
(object | null) !== null
-> object
'foo' in object
-> {foo: unknown}
typeof {foo: unknown}.foo === 'string'
-> {foo: string}
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)
Thanks for the suggestion, but that's probably not relevant to the discussion.
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.
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.
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.
@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
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.
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.
Whereas with my suggestion works as following:
So again, the main benefit is that F2 rename works (which is a huge plus imo)
Anyway just wanted to share.
Most helpful comment
I would like it if type guards with
unknown
worked a little more like this.I think the type promotion ought to work like so, if at all possible.
typeof unknown === 'object'
->object | null
(object | null) !== null
->object
'foo' in object
->{foo: unknown}
typeof {foo: unknown}.foo === 'string'
->{foo: string}