Home > Net >  Typescript optional property type generic
Typescript optional property type generic

Time:11-20

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

CodePudding user response:

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

CodePudding user response:

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

CodePudding user response:

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

  • Related