2

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.

  • 2
    `has all keys but no additional keys`? It's not subset but the same. – Dean Xu Aug 13 '21 at 06:19
  • If you really need such a "subset", you can use `Parital`. – Dean Xu Aug 13 '21 at 06:20
  • 1
    Can you give an example of two types that would be valid here? Because @DeanXu is right, "all keys but no additional keys" is the _exact same type._ – Alex Wayne Aug 13 '21 at 06:25
  • I assume the OP means really that an object only has keys that are in another object too (which includes the condition that it doesn't have any additional ones). – Mike Lischke Aug 13 '21 at 08:23

2 Answers2

2

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>(){}
Filly
  • 411
  • 2
  • 8
0

This is a JS implementation from this answer, not a typescript solution, but this might help?

function compareKeys(a, b) {
  var aKeys = Object.keys(a).sort();
  var bKeys = Object.keys(b).sort();
  return JSON.stringify(aKeys) === JSON.stringify(bKeys);
}

If the two objects that represent an instance of a type have exactly the same keys and no additional keys, this function will return true.

omeanwell
  • 1,847
  • 1
  • 10
  • 16