1

I am trying to create a generic type for normalizing classes, but my use of Array is not working as I would expect.

 interface IBuilding {
  buildingID: number,
  name: string,
  construction: ("wood" | "concrete" | ""),
  website: string,
  address?: IAddress,
  apartments?: IApartment[]
}

type Normalized<T> = {
  [K in keyof T]:
  T[K] extends number ? number :
  T[K] extends string ? string :
  T[K] extends number[] ? number[] :
  T[K] extends string[] ? string[] :
  T[K] extends Function ? never :
  T[K] extends Array<Object> ? number[] :
  T[K] extends Object ? number :
  T[K]
};

let building: Normalized<IBuilding>;

I would expect the building.apartments type to be number[], but instead it is number.

Thanks in advance for the help!

2
  • do you mean at runtime? can you give an example of how the building object is being made? Commented Aug 30, 2018 at 21:34
  • The object was created pretty straightforward. Just created a new const with the Normalized<IBuilding> type, and hardcoded each of the properties. Matt seems to be right about this being an issue with the fact that its an optional property. Commented Aug 31, 2018 at 14:01

1 Answer 1

1

I'm assuming you are using strictNullChecks because that's the only way I can reproduce the behavior described. Since the apartments field of IBuilding is optional, its effective type is undefined | IApartment[], which does not extend Array<Object> because of the undefined. However, even before Normalized is called, the conditional type T[K] extends Object ? number : T[K] is simplified to number, because the compiler assumes that anything that is internally considered a "type variable" (including unsimplified lookup types) is constrained by the empty object type {}, which is assignable to Object. Clearly this assumption is incorrect if T[K] ends up including null or undefined. I filed an issue.

To get the behavior that I assume you want, you can use a distributive conditional type, which will break up any and all unions in the input, including unions involving null and undefined. I think this should be acceptable for your use case.

type NormalizeOne<T> =
  T extends number ? number :
  T extends string ? string :
  T extends number[] ? number[] :
  T extends string[] ? string[] :
  T extends Function ? never :
  T extends Array<Object> ? number[] :
  T extends Object ? number :
  T;  // not reached due to compiler issue

type Normalized<T> = {
  [K in keyof T]: NormalizeOne<T[K]>;
};

If you don't want to break up all unions, you could use a non-distributive conditional type and instead just add specific cases for unions with undefined:

type NormalizeOne<T> =
  [T] extends [number] ? number :
  [T] extends [number | undefined] ? number | undefined :
  [T] extends [string] ? string :
  [T] extends [string | undefined] ? string | undefined :
  [T] extends [number[]] ? number[] :
  [T] extends [number[] | undefined] ? number[] | undefined :
  [T] extends [string[]] ? string[] :
  [T] extends [string[] | undefined] ? string[] | undefined :
  [T] extends [Function] ? never :
  [T] extends [Array<Object>] ? number[] :
  [T] extends [Array<Object> | undefined] ? number[] | undefined :
  [T] extends [Object] ? number :
  T;  // not reached due to compiler issue

(It's probably possible to remove some of the duplication there by defining an auxiliary type alias.)

Sign up to request clarification or add additional context in comments.

1 Comment

It seems you were right that the issue was with the fact that it was an optional property. But your solution worked great. I don't know enough typescript to have come up with a solution like yours, so thanks!

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.