I have two types (extending a common base type) I want to combine, such that when one of the properties exclusive to a subtype is present, all properties exclusive to it are known to be present (i.e., TypeScript and ESLint can recognize it to be that subtype).
I have found very similar situations in TypeScript: Optional merge types and Can Typescript Interfaces express co-occurrence constraints for properties; however, attempts to implement their solutions result in never
being inferred by the ESLint linter.
I am using ESLint in VS Code for my TS typechecking, and it is raising the errors mentioned.
For example, here I have types ConfigA
and ConfigB
that both extend BaseConfig
.
interface BaseConfig {
fooB: boolean;
optbar?: number;
}
interface ConfigA extends BaseConfig {
fooB: false;
first: number;
}
interface ConfigB extends BaseConfig {
fooB: true;
second: number;
third: number;
}
I wish to combine them in a type like AnyConfig
:
type AnyConfig = /* ... either ConfigA or ConfigB are allowable ... */;
Expected inference behavior:
variable assignments
const cfgA: ConfigA = { // good
fooB: false,
first: 1
};
const cfgB: ConfigB = { // good
fooB: true,
optbar: 42,
second: 2,
third: 3
};
const cfgBadHasBoth: ConfigA = { // fails
fooB: false,
first: 1,
third: 3
};
const cfgBadMissingParam: ConfigB = { // fails
fooB: true,
second: 2
}
const cfgBadFoo: ConfigA = { // fails
fooB: true,
first: 1
}
AnyConfig variable assignment
let someCfg: AnyConfig;
// either should be assignable to it
someCfg = cfgA; // should be ok
someCfg = cfgB; // should be ok
ESLint should infer which are present when faced with conditions like:
if (typeof someCfg.first !== "undefined") {
// someCfg must be ConfigA
let a: number = someCfg.first; // should be ok
let b: number = someCfg.second; // should raise type error
let c: number | undefined = someCfg.optbar; // should be ok
let d: number = someCfg.optbar ?? 70; // should be ok
let e: number = someCfg.optbar; // should raise type error
}
if (someCfg.fooB) {
// someCfg must be ConfigB
let a: number = someCfg.first; // should raise type error
let b: number = someCfg.second; // should be ok
...
}
Actual behavior:
ESLint says ‘AnyConfig’ is inferred to be never
:
following TypeScript: Optional merge types:
type None<T> = {[K in keyof T]?: never}
type EitherOrBoth<T1, T2> = T1 & None<T2> | T2 & None<T1> | T1 & T2
// problem: eslint thinks "type AnyConfig = never"
type AnyConfig = EitherOrBoth<ConfigA, ConfigB>;
trying to be explicit by expanding instead of subtyping, opposing DRY:
// expanded definitions
interface ConfigA {
fooB: false;
optbar?: number;
first: number;
}
interface ConfigB {
fooB: true;
optbar?: number;
second: number;
third: number;
}
// problem: eslint thinks "type AnyConfig = never"
type AnyConfig = EitherOrBoth<ConfigA, ConfigB>;
Attempting the AnyConfig variable assignment
ESLint should recognize the type ConfigA
is allowable, but instead it produces the error “Type ‘ConfigA’ is not assignable to type ‘never’“:
let someCfg: AnyConfig = cfgA; // fails, "not assignable to type 'never'"
How can I define AnyConfig such that let someCfg: AnyConfig = cfgA;
works as expected, inference and all?
5