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>(){}