3

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.

  • Your `Residual` makes any definition in base completely useless, as the `numberProperty` and `booleanProperty` are also keys for an object, maybe you could find a way to exclude those keys from the `Residual` definition? Though if you could maybe simply move where the other properties are defined on the object :) – Icepickle May 16 '20 at 08:46
  • Does this one help: https://stackoverflow.com/questions/58216298/how-to-omit-keystring-any-from-a-type-in-typescript – Icepickle May 16 '20 at 08:49

1 Answers1

1

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;
satanTime
  • 12,631
  • 1
  • 25
  • 73
  • 1
    Thanks, this was exactly what I was looking for! – Ville Brofeldt May 17 '20 at 05:26
  • Actually, the proposed answer introduces a side-effect of making all properties optionally string. Adding the following to the proposed change triggers another error: ``` const booleanFunc = (bool: boolean) => { return bool; } const x = booleanFunc(abc.booleanProperty); ``` Error: ``` Argument of type 'string | boolean' is not assignable to parameter of type 'boolean'. Type 'string' is not assignable to type 'boolean'.ts(2345) ``` – Ville Brofeldt May 18 '20 at 07:25
  • have you removed question mark after `[K in keyof any]`? I think it was the latest change yesterday. – satanTime May 18 '20 at 07:42
  • and `[key: string]: string` does exactly that - it says all properties are strings. All custom fields should be defined in `Base` otherwise you should remove `Residual` because its signature forces everything to be strings. – satanTime May 18 '20 at 07:44
  • Yes, I do believe I've copied it over correctly. I'm ok with this, although it would be great to somehow keep the types defined in `Base` unaffected by `Residual` when expecting an object of `Complete` type. Perhaps this will be added in a future version of TypeScript. – Ville Brofeldt May 18 '20 at 09:58
  • Ah, I see what you mean, then `never` can help here, I've updated the answer. Now `Residual` doesn't have keys from `Base` at all. – satanTime May 18 '20 at 10:00
  • I tried with the updated code, but `const booleanVariable: boolean = abc.booleanProperty;` still complains `Type 'string | boolean' is not assignable to type 'boolean'. Type 'string' is not assignable to type 'boolean'.ts(2322)` – Ville Brofeldt May 18 '20 at 16:43
  • Now I see what you mean, yeah TS is tough time to time. – satanTime May 18 '20 at 16:50
  • I added to the answer one more option, it's not the best one, but it's the only way I know. – satanTime May 18 '20 at 17:05
  • This works perfectly, thanks a lot for all the help! – Ville Brofeldt May 19 '20 at 06:43
  • Glad to hear! Happy coding! – satanTime May 19 '20 at 06:43