Natasha
Natasha

Reputation: 816

Issue with TypeScript typing

Please have a look at the following TypeScript snippet. Why does that NOT throw a compile error? Isn't that obviously a type error? What do I have to change to make it type-safe again? TYVM

type A<P> = {
  p?: never,
  q?: Partial<P>
}

type B<P> = {
  p?: Partial<P> 
  q?: never
}

type C<P> = A<P> | B<P>

const c: C<{ a: number}> = {
  p: {
    a: 1,
    b: 2   // <------ Why is this allowed?!?
  }
}

console.log(c)

Click here for a demo

Upvotes: 4

Views: 602

Answers (1)

jcalz
jcalz

Reputation: 330316

This is a known bug where excess property checking doesn't apply to nested types involving unions and intersections in the way that people expect. Excess property checking is kind of an add-on to the type system that only applies to object literals, so when it doesn't apply, things fall back to the structural subtyping rule where type {a: A, b: B} is a subtype of {a: A}, and so a value of the former type should be assignable to a variable of the latter type. You might want to head over to the issue in Github and give it a 👍 or explain your use case if you think it's more compelling than the ones already listed there. Hopefully there will be a fix someday.

Until then, though, I'm not sure what can be done. The type-level equivalent to excess property checks are so-called exact types, which don't exist in TypeScript as concrete types. There are ways to simulate them using generic helper functions and type inference... in your case it would look something like this:

type Exactify<T, K extends keyof any>
  = T & { [P in Exclude<K, keyof T>]?: never };

type A<P, K extends keyof any=never> = {
  p?: never,
  q?: Partial<Exactify<P, K>>
}

type B<P, K extends keyof any=never> = {
  p?: Partial<Exactify<P, K>>
  q?: never
}

type C<P, K extends keyof any = never> = A<P, K> | B<P, K>

type KeyofKeyof<T> =
  keyof T | { [K in keyof T]: T[K] extends object ? keyof T[K] : never }[keyof T];

const asC = <T extends C<{ a: number }, KeyofKeyof<T>>>(c: T) => c;

const c = asC({
  p: {
    a: 1,
    b: 2   // <------ error
  }
})

Yes, it's ugly. Not sure if it's worth it to you. The way it works is to say that C<P, K> is the same as your C<P> except that the P type is augmented to explicitly exclude all the extra properties in K. Then we use the helper function asC() to infer a type for K given the passed-in parameter. Since the keys in question are nested one level down, I needed a KeyofKeyof<T> type to extract keys from one level down (you can't do it for all levels without hitting circular types).

The desired error shows up now. Yay? I guess.

Anyway hope that helps. Good luck!

Upvotes: 1

Related Questions