Reputation: 68
I am trying to define a type which accepts residual properties as follows:
export type Base = {
numberProperty: number;
booleanProperty: boolean;
};
export type Residual = {
[key: string]: string;
};
export type Complete = Base & Residual;
const abc: Complete = {
numberProperty: 1234,
booleanProperty: true,
residualProperty: 'abc',
};
In other words, I want to make sure numberProperty
and booleanProperty
are always of number
and boolean
type respectively, but any other property should be string
. However, when compiling this (3.9.2), I get the following error:
error TS2322: Type '{ numberProperty: number; booleanProperty: true; residualProperty: string; }' is not assignable to type 'Complete'.
Type '{ numberProperty: number; booleanProperty: true; residualProperty: string; }' is not assignable to type 'Residual'.
Property 'numberProperty' is incompatible with index signature.
Type 'number' is not assignable to type 'string'.
13 const abc: Complete = {
~~~
Found 1 error.
I've found similar questions and documentation that addresses similar issues, but I have yet to find a conclusive answer of how to do this.
Upvotes: 3
Views: 437
Reputation: 13574
The problem is that Residual
has a conflict with keys numberProperty
and booleanProperty
from Base
, because they aren't string.
To fix it you need to change Residual
to say that everything except Base
is string. After that Residual
respects Base
and they can be combined together.
export type Base = {
numberProperty: number;
booleanProperty: boolean;
};
export type Residual = {
[K in keyof any]: K extends keyof Base ? never : string;
};
export type Complete = Base | Residual;
const abc: Complete = {
numberProperty: 1234,
booleanProperty: true,
residualProperty: 'abc',
};
If you can't change Residual
, then you should reconsider your code to avoid their union because it has a conflict and won't be valid.
const val1: Residual = {
numberProperty: 'string', // valid, positive
};
const val2: Base = {
numberProperty: 'string', // invalid, negative
};
const val3: Residual | Base = {
numberProperty: 'string', // positive & negative = negative.
};
for a deep type respect we have to omit string
and always operate with defined keys. Usually people have 2 options - specify all keys or to use a validation function.
export type Base = {
numberProperty: number;
booleanProperty: boolean;
};
export type Residual<KEYS extends keyof any = keyof any> = {
[K in KEYS]: string;
};
export type Complete<K extends keyof any> = Base & Residual<Exclude<K, keyof Base>>;
const abc: Complete<'residualProperty'> = {
numberProperty: 1234,
booleanProperty: true,
residualProperty: 'abc',
};
const booleanVariable: boolean = abc.booleanProperty;
const validateComplete = <T extends Complete<K>, K extends keyof T>(value: T): Complete<K> => value;
const abc2 = validateComplete({
numberProperty: 1234,
booleanProperty: true,
residualProperty: 'abc',
});
const booleanVariable2: boolean = abc2.booleanProperty;
const stringVariable2: string = abc2.residualProperty;
Upvotes: 1