Ville Brofeldt
Ville Brofeldt

Reputation: 68

How to combine index types with intersection types

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

Answers (1)

satanTime
satanTime

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

Related Questions