Reputation: 31
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
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
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
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.
function exactMatch<A extends C, B extends A, C = B>(){}
Upvotes: 2