When using a custom typeof‑like function, I’d like TypeScript compiler to be able to infer the correct type in the scope guarded by the custom typeof‑like function.
I need this to provide proper type information for the type function from Blissfuljs and similar projects.
The current approach requires defining the type(…) function as type(obj: any): string and then doing a type cast every time something is accessed within the if block:
/// <reference types="blissfuljs"/>
declare var something: any;
if ($.type(something) === "array") {
(something as any[]).forEach(v => {/* stuff */})
}
type-func.d.ts
/**
* @param obj The variable to check the type of.
* @return The result of `typeof obj` or the class name in lowercase for objects.
* In the case of numbers, if the value is `NaN`, then the result is `nan`.
*/
declare function type(obj: null): "null";
declare function type(obj: undefined): "undefined";
// This is to ensure that the type system short-circuits when it encounters primitive types.
declare function type(obj: number): "number" | "nan";
declare function type(obj: string): "string";
declare function type(obj: symbol): "symbol";
declare function type(obj: boolean): "boolean";
// Needed to ensure proper return values when wrapper objects are used.
/* tslint:disable:ban-types */
declare function type(obj: number | Number): "number" | "nan";
declare function type(obj: string | String): "string";
declare function type(obj: symbol | Symbol): "symbol";
declare function type(obj: boolean | Boolean): "boolean";
declare function type(obj: Function): "function";
/* tslint:enable:ban-types */
declare function type(obj: any[]): "array";
declare function type(obj: RegExp): "regexp";
declare function type(obj: any): string;
export = type;
example.ts
import type = require("./type-func");
declare var something: any;
if (type(something) === "array") {
// $ExpectType any[]
something;
} else if (type(somthing) === "number") {
// $ExpectType number
something;
}
My suggestion meets these guidelines:
Why are regular custom type guards not enough ?
function type(something: any, t: "array") : something is Array<any>
function type(something: any, t: "number") : something is number
function type(something: any, t: string) : boolean {
return true // fancy logic here
}
declare var something: any;
if (type(something, "array")) {
// is any[]
something;
} else if (type(something, "number")) {
// is number
something;
}
The syntax is slightly different but it serves the same use case.
Because I’m using a JavaScript library that doesn’t work that way: https://blissfuljs.com/docs.html#fn-type
What would this look like?
// new syntax construct maybe(type1, param is type2):
// * can be used whenever the "is" clause can be used
// * if the type of the return is narrowed to type1, then, param is type2
declare function JQueryType(v: any):
maybe("string", v is string)
& maybe("number", v is number)
& maybe("array", v is any[]);
I would expect:
/**
* @param obj The variable to check the type of.
* @return The result of `typeof obj` or the class name in lowercase for objects.
* In the case of numbers, if the value is `NaN`, then the result is `nan`.
*/
declare function type(obj: null): "null";
declare function type(obj: undefined): "undefined";
// This is to ensure that the type system short-circuits when it encounters primitive types.
declare function type(obj: number): "number" | "nan";
declare function type(obj: string): "string";
declare function type(obj: symbol): "symbol";
declare function type(obj: boolean): "boolean";
// Needed to ensure proper return values when wrapper objects are used.
/* tslint:disable:ban-types */
declare function type(obj: number | Number): "number" | "nan";
declare function type(obj: string | String): "string";
declare function type(obj: symbol | Symbol): "symbol";
declare function type(obj: boolean | Boolean): "boolean";
declare function type(obj: Function): "function";
/* tslint:enable:ban-types */
declare function type(obj: any[]): "array";
declare function type(obj: RegExp): "regexp";
declare function type(obj: any): string;
export = type;
to just work.
I see. If you'd like it to work that way, https://github.com/Microsoft/TypeScript/issues/14107 is related (but there's more to this).
@ExE-Boss There isn't enough in the type system to know that 'regex' is tied to the Regex type, and overloads don't seem like a good choice to encode this behavior . I would propose an extension to the type guard syntax. Add the capability to specify the return value when a parameter is of certain type
declare function type(obj: any):
obj is number? "number" | "nan" :
obj is string ? "string":
obj is symbol ? "symbol"
After ? you can have a literal type or a union of literal types. Omitting ? defaults to obj is T ? true: obj is Exclude<typeof obj, T>? false which is the current behavior.
</wacky-syntax-proposal >
Agree with @dragomirtitian
Sometimes I need this:
// If obj is Animal, return it's name (and obj is treated as Animal)
// otherwise return false( and obj is treated as not Animal)
function getNameIfIsAnimal(obj: any):
obj is Animal ? string : false {
// ...
}
@dragomirtitian
After
?you can have a literal type or a union of literal types. Omitting?defaults toobj is T ? true: obj is Exclude<typeof obj, T>? falsewhich is the current behavior.
Actually, it should default to obj is T ? true : false, since you have to have a : in a ternary expression.
Also, the following is invalid:
declare function type<T>(obj: T):
T extends number ? "number" | "nan" :
T extends string ? "string" :
T extends symbol ? "symbol"
// ~ error TS1005: ':' expected.
and presumably:
declare function type(obj: unknown):
obj is number ? "number" | "nan" :
obj is string ? "string" :
obj is symbol ? "symbol"
// ~ error TS1005: ':' expected.
would print the same error.
@ExE-Boss It looks like ternary expression, but it is not really meant to be that, it's an extension of the type guard syntax. The default branch is whatever remains after the known type cases are exhausted.
declare function type(obj: unknown):
obj is number ? "number" | "nan" :
obj is string ? "string" :
obj is symbol ? "symbol"
declare obj: number | string | symbol | { o: number }
switch(type(obj)) {
case "number":
case "nan":
//obj is number
break;
case "string":
//obj is string
break;
case "symbol":
//obj is symbol
break;
default:
// obj is whatever there is left
}
This is similar to the way the current type guard syntax only defines what happens on the true case, the false case is derived implicitly by exclusion.
Perhaps the usage of ?: was not the most fortunate, it was just a proposal, another syntax can be chosen, but IMO it should be an extension of the current type guard syntax. ?: seemed to fit at the time but may cause confusion.
Yeah, but then the return type of the type function would be unknown, since the default return value is undeclared, so it’s a syntax error.
Most helpful comment
@ExE-Boss There isn't enough in the type system to know that
'regex'is tied to theRegextype, and overloads don't seem like a good choice to encode this behavior . I would propose an extension to the type guard syntax. Add the capability to specify the return value when a parameter is of certain typeAfter
?you can have a literal type or a union of literal types. Omitting?defaults toobj is T ? true: obj is Exclude<typeof obj, T>? falsewhich is the current behavior.</wacky-syntax-proposal >