David Kváš
David Kváš

Reputation: 31

How to check if a type has all keys from another type but no additional keys?

I want to check if one type has all keys from another type but no additional keys, e.i. it's a subset. Basically, I need some function like the one below which would give me the typescript error if the TTo is not a subset.

function typeCheck<TFrom, TTo>(arg: TFrom): TTo {
   return arg;
}

Any help would be much appreciated.

Upvotes: 3

Views: 1672

Answers (2)

Ivan Akcheurov
Ivan Akcheurov

Reputation: 2373

This answer looks more readable and relies on pure types only:

type ShapeOf<T> = Record<keyof T, any>
type AssertKeysEqual<X extends ShapeOf<Y>, Y extends ShapeOf<X>> = never

type Assertion = AssertKeysEqual<{a:1}, {a:1, b: 'x'}>
// ERROR: Property 'b' is missing in type '{ a: 1; }' but required in 
// type 'ShapeOf<{ a: 1; b: "x"; }>'.

Upvotes: 0

Filly
Filly

Reputation: 439

I had the same problem today and came up with this solution:

function typeCheck<TTo, TFrom = TTo>(arg: TFrom): TTo {
  return arg as unknown as TTo;
}
type ToTest = { a: string }

const exactMatch = typeCheck<ToTest>({ a: "a" }) // valid
const moreKeysInArgs = typeCheck<ToTest>({ a: "a", b: "b" }) //error
const moreKeysinTo = typeCheck<ToTest & { b: string }>({ a: "b" }) //error

You can see a code example on TS Playground here

Edit:

So i played around a bit and i noticed that if you define your object seperately typescript can't infer the type correctly

function typeCheck<A, B = A>(arg: B): A {
    return arg as unknown as A;
}
type ToTest = { message: { a: string, b: string, c: string } }


//exactMatch
typeCheck<ToTest>({ message: { a: "", b: "", c: "" } }) // valid
const moreKeysInArgs = typeCheck<ToTest>({ message: { a: "", b: "", c: "", e: "" } }) //error
const moreKeysinTo = typeCheck<ToTest & { message: { e: string } }>({ message: { a: "", b: "", c: "" } }) //error

const y = ({ message: { a: "", b: "", c: "", e: "" } })
// inferred type of typeCheck: typeCheck<ToTest,ToTest>
const noError = typeCheck<ToTest>(y) //no error

You have to do it like this:

function exactMatch<A extends C, B extends A, C = B>(){}

exactMatch<ToTest,typeof x>() //valid
exactMatch<ToTest,typeof y>() //invalid 
exactMatch<typeof y,ToTest>() //invalid

So now you may ask yourself why do I need a third generic. Imagine a function that checks if objA extends objB like

function Extends<A,B extends A>(){}
// valid, because every Key and Type of A matches B
Extends<{a:string,b:string}, {a:string,b:string,c:string}>()
// invalid because c is missing in the generic B
Extends<{a:string,b:string,c:string}, {a:string,b:string}>() 

So now you may want to apply the contstraint that A extends B, but that won't work because you will create an error

function Extends<A extends B,B extends A>(){}
type parameter 'A' has a circular constraint.

And now the fun part: Typescript doesn't seem to have any issues when you bind the generic B on C first and ask A to extend C afterwards. I really don't know why that happens and why the first solution doesn't work like expected.

If I find an answer I will keep you updated.

Solution

function exactMatch<A extends C, B extends A, C = B>(){}

Upvotes: 2

Related Questions