Make TypeScript type system invariant
Type compatibility in TypeScript is based on structural subtyping.
Therefore, there are some limitations in narrowing the types of Object methods like Object.keys
or Object.entries
.
Further reading
However, if use invariant type system, the following typing is possible.
export type ObjectKey<O extends object> = Exclude<keyof O, symbol>;
/**
* Using declaration merging feature
*/
declare global {
export interface ObjectConstructor {
getOwnPropertyNames<T extends object>(o: InvariantOf<T>): Array<ObjectKey<T>>;
keys<T extends object>(o: InvariantOf<T>): Array<ObjectKey<T>>;
entries<T extends object>(o: InvariantOf<T>): Array<[ObjectKey<T>, T[ObjectKey<T>]]>;
}
}
It has similar benefit to using a Nominal Type System.
But, no needs to brand
Make object type invariance.
- Invariance does not accept supertypes.
- Invariance does not accept subtypes.
import {invariantOf, InvariantOf} from 'invariant-of';
interface Base {
foo: string;
}
interface Derived extends Base {
bar: string;
}
declare function method1(value: Base): void;
declare function method2(value: InvariantOf<Base>): void;
method1({foo: 'foo'} as Base); // Okay
method1({foo: 'foo', bar: 'bar'} as Derived); // Okay
method2({foo: 'foo'} as InvariantOf<Base>); // Okay
method2(invariantOf({foo: 'foo'})); // Okay
method2({foo: 'foo', bar: 'bar'} as InvariantOf<Derived>); // Error
Here is a comparison with default behavior.
It does not affect runtime behavior.
npm install invariant-of
interface Base {
foo: number;
bar?: string;
}
interface Derived extends Base {
baz: string;
}
const someObject: Base = {foo: 123, bar: 'hello'};
const derivedObject: Derived = {foo: 123, bar: 'hello', baz: 'bye'};
function getKeys(args: InvariantOf<Base>) {
return Object.keys(args);
}
getKeys(someObject); // Error
getKeys(derivedObject); // Error
getKeys(invariantOf(someObject)); // Work
getKeys(invariantOf(derivedObject)); // Error