¿Hay alguna manera de hacer que este estrechamiento de TypeScript sea menos feo (o más elegante ;-) ❓
Con la protección de tipo IsSomething , quiero restringir el acceso a métodos y propiedades comunes de cualquier variable de JavaScript que no sea null ni undefined (cosas como .toString , .valueOf , .hasOwnProperty , .constructor , etc.)
export type Something = number | string | boolean | symbol | object | bigint; export function IsSomething(value: unknown) : value is Something { return value !== undefined && value !== null; } let a = [1, "2", 3]; Narrowing(a); NotNarrowing(a); function Narrowing(o: unknown) { // OK if (IsSomething(o)) { console.log(o.toString(), o.constructor); } } function NotNarrowing(o: unknown) { // NOT OK if (o !== null && o !== undefined) { console.log(o.toString(), o.constructor); } }En TypeScript, todos los valores, excepto null e undefined , se pueden asignar al llamado tipo de objeto vacío {} . Entonces puede simplificar su Something a solo {} y su función de protección de tipos definida por el usuario funcionará igual que antes:
export type Something = {}; function isSomething(value: unknown): value is Something { return value !== undefined && value !== null; } function narrowing(o: unknown) { // OK if (isSomething(o)) { console.log(o.toString(), o.constructor); } } Además, TypeScript 4.8 introducirá soporte mejorado para reducir unknown , después de lo cual su función "NotNarrowing" comenzará a funcionar, porque al marcar (o !== null && o !== undefined) reducirá o de unknown a {} automáticamente:
// TS4.8+ function nowNarrowing(o: unknown) { // okay if (o !== null && o !== undefined) { console.log(o.toString(), o.constructor); } }La excelente respuesta de @jcalz me llevó a la siguiente solución:
// With `IsSomething` type guard, I want to narrow down access // to common properties of any JS variable // which is not null and not undefined, like: // .toString, .valueOf, .hasOwnProperty, .constructor etc. export function IsSomething(value: unknown) : value is Object { return value !== null && value !== undefined; } Narrowing([1, 2, 3]); function Narrowing(o: unknown) { // OK if (IsSomething(o)) { console.log(o.toString(), o.constructor); } } Resulta que TypeScript tiene una interface Object , que funciona exactamente como yo quiero:
En TypeScript, la interfaz de Object es diferente del tipo de object primitivo, ya que el primero todavía se puede asignar a cualquier otro tipo primitivo o complejo. También es diferente de un tipo vacío como type Something = {} , ya que este último no es compatible con IntelliSense.
// Object interface is OK let a: Object = 42; let b: Object = true; let c: Object = "string"; let d: Object = Symbol(42); let e: Object = new class SomeClass { someProp = 42 }; // Empty type is OK but no IntelliSense let a1: {} = 42; // Error: Type 'number' is not assignable to type 'object' let a2: object = 42; Aún queda un problema sutil. Como señaló @CertainPerformance, lo anterior no funcionará para un caso extremo como Object.create(null) , donde se crea un objeto que no tiene propiedades ni métodos, pero tampoco es null .
Si ese es un requisito, lo siguiente debería hacer el trabajo:
// can be extended to check for toString, valueOf, etc export function IsSomething(value: unknown) : value is Object { return (<Object>value)?.constructor !== undefined; } Narrowing([1, 2, 3]); function Narrowing(o: unknown) { // OK if (IsSomething(o)) { console.log(o.toString(), o.constructor); } }