0

I want to be able to create a new type DataObject that simply omits the id property of an interface that extends the Model interface. DataObject should therefore require all properties of T, except for id, and prohibit any other properties. I have the following types/interfaces defined:

type DataObject<T extends Model> = Omit<T, "id">

interface Model extends JSONObject {
  id: string
}

interface JSONObject {
  [key: string]: JSONValue
}

type JSONArray = JSONValue[]

type JSONValue = PrimitiveValue | JSONObject | JSONArray

type PrimitiveValue = string | number | boolean

Then I have several functions that accept data objects of a given type and inserts the data into a database, in a similar pattern to this:

// identifier is the table name, optionally with the id provided "tablename:id"
function insert<T extends Model>(identifier: string, data: DataObject<T>) {
  //...inserts data
}

But, when I use the function, it does not enforce the typing I want it to; I want it to give an error if I try to give it a data object containing any properties not defined in a given interface that extends Model, (except for id), and if it is missing any properties (except for id).

interface Person extends Model {
  name: string,
  age: number
}

// this should not be valid
insert<Person>({
  foo: "bar",
  id: "a1b2"
})

// This should not be valid either
const personData: DataObject<Person>{
  name: "John",
  id: "12345"
}

If DataObject simply takes any type T it works, but I want it to specifically be an extension of Model. Is this possible somehow?

Some more context: The reason why I want id to be excluded when inserting data is that the database takes care of creating the record id, but when retrieving records from the database the id property is present, hence why Model always has it.

I've tried defining my own Omit type and using it instead. Also tried defining the DataObject type directly in the function definiton.

Noobster
  • 83
  • 1
  • 8
  • 2
    This is just how TypeScript works, "duck typing" means that everything that is assignable to a variable in terms of shape can be assigned to it. That's why an `Anything` will always be assignable to a `DataObject` . – Guerric P Nov 20 '22 at 17:25
  • 1
    Does [this approach](https://tsplay.dev/mx80Zm) meet your needs? You can explicitly prohibit the `id` key. It is not possible to implicitly prohibit all unknown keys, though, because of structural subtyping... but I don't see an example where it matters in your question, so possibly [excess property checking](https://www.typescriptlang.org/docs/handbook/release-notes/typescript-1-6.html#stricter-object-literal-assignment-checks) is sufficient. If this fully addresses the question I can write up an answer explaining; if not, what am I missing? – jcalz Nov 20 '22 at 17:35
  • @jcalz I don't think that meets my needs I'm afraid, as it is important that only the exact properties of the given interface are present, excluding id, and no others. The context here is that these are all functions that insert that into a database, and I'm trying to enforce strict typing. – Noobster Nov 20 '22 at 17:37
  • 2
    You can never actually completely avoid excess properties in TypeScript; structural subtyping requires excess properties not break compatibility, so what you're asking for isn't about "strict"ness, but about whether you can "seal" or "close" an object type. TS doesn't give this, so all you can do is discourage excess properties. If pushing an excess property into a database is catastrophic, you will need to refactor your code to have a runtime check of the keys. – jcalz Nov 20 '22 at 17:40
  • Let me write up a different suggestion which gets as close as I can imagine getting in TS, hold on – jcalz Nov 20 '22 at 17:42
  • @jcalz I edited the question now, giving slightly more insight into my problem. The solution I had before was to not create interfaces, but types that included all the data fields and the id. Doing this I got the strict typing I desired, but I would like it to work with interfaces. – Noobster Nov 20 '22 at 17:50
  • 1
    So [this](https://tsplay.dev/N5zbBm) is the closest I can get without the refactoring with runtime key checking. There is no specific type in TypeScript that says "prohibit all possible excess properties", so the best you can do is a generic constraint. Does that fully address the question? If so I could write up the answer; if not, what is left to address? – jcalz Nov 20 '22 at 18:02
  • @jcalz This is actually really close to what I want, thanks! It seems to give some kind of error at the second U here though: const dataObject = () => (o: DataObject) => o; Not sure what to do about that, but I think I'll just go with types instead of interfaces as I described, because that 100% fills my need except it doesn't allow me to extend Model. – Noobster Nov 20 '22 at 18:08
  • Oh, sorry, like [this](https://tsplay.dev/m0n8qw) then? Same question about whether this fully addresses the question or not – jcalz Nov 20 '22 at 18:11
  • @jcalz I deleted my answer as I realised it didn't work as I thought, it still allowed unspecified properties. I still need a way for this to work without returning a new function though. – Noobster Nov 20 '22 at 18:25
  • @jcalz I'm understanding less and less, in my actual code the solution using type Person instead works fine, in the sandbox it does not. – Noobster Nov 20 '22 at 18:31
  • “I still need a way for this to work without returning a new function though” the word “still” there, did you mention that somewhere before now? – jcalz Nov 20 '22 at 18:37
  • Okay, now that I'm looking at your answer, let me back up and not worry about excess properties or `Omit` per se, but just the interface-vs-type thing. You can change `Model` so that instead of having an index signature (which always accepts "excess" properties no matter what) you just check the type for validity, like [this](//tsplay.dev/NdrLnW). Does *that* meet your needs? If so then I could write up an answer but hopefully you'll [edit] the question to make it clear that it's the `Person extends Model` part that is your problem and not trying to `Omit` from a type with an index signature – jcalz Nov 20 '22 at 18:48

1 Answers1

0

Just to provide an answer as close to my use case as possible, this is what I'm doing for now:

type Person = {
  name: string,
  age: number,
  id: string
}

It does allow you to forget to include the id attribute when creating the type that represents a model, but the function definitions cause typescript to give a warning abut this, which is what I wanted.

Example

Noobster
  • 83
  • 1
  • 8
  • Can you explain how this has to do with types vs interfaces? If you write `interface Person {name: string; age: number; id: string}` [is anything different](https://tsplay.dev/ND2n4W)? It's not clear how this fits your needs in the question because `Person extends Model` is false, so you get errors. – jcalz Nov 20 '22 at 18:13
  • @jcalz Let me do some more testing and I'll come back with some examples. It's not giving me any errors when using type Person in the function, I'm guessing as it conforms to the definition of Model? It does provide an error though if Person does not include id, which is exactly what I'd expect. – Noobster Nov 20 '22 at 18:16
  • @jcalz Resubmitted my answer now after some more testing, please see Example in the answer as URL was too long for a comment – Noobster Nov 20 '22 at 18:35
  • 1
    Ah, okay, I see the interface-vs-type issue. I will address that above. But see [this](https://tsplay.dev/mZXlow), which uses your type, and still allows excess properties to creep in, because the feature you're relying on only works on object literals; it's not intended to *seal* interfaces to prevent excess keys from causing problems somewhere. It's more of a linter rule to lower the chance that you forget about extra properties. – jcalz Nov 20 '22 at 18:45
  • @jcalz Yeah I see what you mean, it's definitely not perfect, but you would have to go slightly out of your way to get around it – Noobster Nov 20 '22 at 18:53
  • @Noobster oh, I see the problem. Interfaces and types are different in terms of indexing. Please see [this](https://stackoverflow.com/questions/37233735/interfaces-vs-types-in-typescript#answer-64971386) question and my [article](https://catchts.com/safer-types#part_2) – captain-yossarian from Ukraine Nov 21 '22 at 13:01