1

I would like my function to take any kind of objects but if the objet have a property 'id' making sure that is is either a string or a number.

Here is the minimal example:

interface FnItem {
  id?: string | number;
};

function fn<T extends FnItem>(item: T, callback: (item: T) => void) {
  console.log(item.id);
  callback(item)
};

fn({ name: 'Michel' }, item => item.name);
fn({ name: 'Michel', id: 12 }, item => item.name);

It throws this error

Argument of type '{ name: string; }' is not assignable to parameter of type 'FnItem'.
  Object literal may only specify known properties, and 'name' does not exist in type 'FnItem'
---
Property 'name' does not exist on type 'FnItem
3
  • I'm not sure what your question is - could you edit to clarify? Commented Nov 19, 2021 at 8:50
  • fn needs to specify the generic so should be fn<YourType>(). If you just want an interface then you don't need the generic. The signature can simple be fn(item:FnItem) Commented Nov 19, 2021 at 8:54
  • I have clarify the minimum example, I hope it is clearer Commented Nov 19, 2021 at 9:07

3 Answers 3

1

Assuming that FnItem might be either any object with any properties or any object where id is number|string I would rather stick with this solution:

type FnItem = Record<string, unknown>

type IdValidation<Obj extends Record<string, unknown>> =
    Obj extends { id: infer Id } ? Id extends string | number ? Obj : Obj & { id: never } : Obj;

function fn<T extends FnItem,>(item: IdValidation<T>, callback: (item: IdValidation<T>) => void) {
    console.log(item.id);
    callback(item)
};

fn({ name: 'Michel' }, item => item.name);
fn({ name: 'Michel', id: 12 }, item => item.id);

fn({ name: 'Michel' }, item => item.ya); // error
fn({ name: 'Michel', id: [] }, item => item.id); // id is highlighted as a wrong property

Playground

Since first argument might be any object, we should allow passing Record<string,unknown> which in turn disables our constraint regarding id being number|string. This is why I have added IdValidation utility type. It just checks whether id property meets condition or not. If it meets - leave id as is, otherwise - replace id type with never. Using never allows you to highlight only incorrect property which makes it easy to read and understand.

If you are interested in TS validation techniques you can check my articles here and here

Sign up to request clarification or add additional context in comments.

1 Comment

For anyone tying to understand what's going on in there see TypeScript conditional types
1

The error is telling you all you need. name doesn't exist on your FnItem type. You can fix it by adding the property or adding an index signature if you want to add arbitrary keys like so:

interface FnItem {
  id?: string | number;
  [key: string]: any; // or whatever types you accept
};

As for the generic, I can't tell what you need it for at the moment, as you could simply define the function as

function fn(item: FnItem): void

2 Comments

You don't need to implement everything in the interface. That kinda defaults the object of interfaces
It works however as @Liam say, I don't want to allow accessing other property of the object. For example function fn<T extends FnItem>(item: T, callback: (item: T) => void) { console.log(item.yaya); }; It should throw an error. With your solution it just allow it.
0

If you want to pass a generic then you need to tell the function what concrete type implements your interface:

interface FnItem {
  id?: string | number;
};

function fn<T extends FnItem>(item: T,  callback: (item: T) => void) {
  console.log(item.id);
  callback(item);
};

fn<myType>({ name: 'Michel', id: 12 }, (item:myType)  => { console.log(item.name); });
fn<myType>({ name: 'Michel' }, (item:myType) => { console.log(item.name); });

class myType implements FnItem
{
    name: string = "";
    id?: number;
}

Compiled example

10 Comments

I need the generic, I have updated my question to make it clearer. I don't want to define the possible property because I want this function to accept "any" type.
Added example for you answer. You still need to specify what exactly T is
Shouldn't it work without specifying what T is. It can determine what T is because it is the first argument. The same way it works for fn({ name: 'Michel', id: 12 }, item => item.name);
The generic can sometimes be implied. And in fact in the above the compiler will figure it out, but all this is doing is the compiler populating the generic for you. You still need a concrete type if your using a generic. That's why generics exist!
Okay, however it still don't work. Let's say my type is interface OnlyName { name: string; }, is still throw an error Type 'OnlyName' has no properties in common with type 'FnItem'.
|

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.