Home > Net >  How to require a specific data type in a class or object in TypeScript?
How to require a specific data type in a class or object in TypeScript?

Time:10-31

I know there are Record or Pick, but I don't know how to use them well or I don't fully understand. Well, what I want to achieve is that in a class or object at least it has a specific data type, for example, the class or object can contain strings, Booleans, arrays, etc., but it must necessarily have at least properties or fields with a Function type.

interface TypeAtLeastRequired {
    [index: string]: Function;
}

class Foo implements TypeAtLeastRequired { //<-- Error: This class must have at least one function
    msg: string = 'Hello!';
}

// or something like that:

type IsRequired<C> = {
    [K in keyof C]: C[K] ThatAtLeastBe Function;
};

I wish the same for literal objects ({})

Is it possible to do something like that with TypeScript?

I hope you can help me understand or find a possible solution :)

CodePudding user response:

There is no specific type in TypeScript that corresponds exactly to the set of JavaScript objects with at least one function-valued property.

Index signatures don't work because they don't require any properties at all (the empty object {} is assignable to {[k: string]: Function}) and because they require that any property that is present must be of a particular type (so the object {a: ()=>{}, b: 0} is not assignable to {[k: string]: Function} because the b property is not a function). So we should probably give up on index signatures.

Instead we could express what you're doing as a generic constraint, and a fundamentally recursive constraint (sometimes known as F-bounded polymorphism). So instead of TypeAtLeastRequired being a specific type, you do something like T extends TypeAtLeastRequired<T>. The first T represents the type you're checking, and TypeAtLeastRequired is a type function you apply to T and get something out. If you build that type function correctly, you can make it so that T corresponds to an object with at least one function-valued property if and only if T extends typeAtLeastRequired<T>.


Here's one possible implementation:

type TypeAtLeastRequired<T> = (
  { [K in keyof T]-?: T[K] extends Function ? unknown : never }[keyof T]
) extends never ? { "please add at least one function prop": Function } : T

If T has at no function-valued properties, then the mapped type {[K in keyof T]-?: T[K] extends Function ? unknown : never}[keyof T] will have all its properties be the never type. Otherwise, at least one property will not be the unknown type and not never.

By indexing into that mapped type with keyof T, we get the union of all its properties; if none of T's properties were functions, then that union will be never (never | never is also never). If at least one property is a function, then the union will be unknown (never | unknown is unknown).

Then we use a conditional type to check this new unknown or never value against never. If we had no function properties, this is never, and so we finally get the true branch of the conditional: {"please add...": Function}. If we had at least one function properties, this is unknown, and so we finally get the false branch of the conditional: T.

So in the case where T has no function-valued properties, TypeAtLeastRequired<T> will be {"please...": Function}, and T extends {"please...": Function} is not true. But in the case where it has at least one function-valued property, TypeAtLeastRequired<T> will be T, and T extends T is true.

So you can see how T extends TypeAtLeastRequired<T> expresses the desired constraint.


So now, let's use it. If you want to use it in an implements clause, you have to mention the implementing class name twice. It's repetitive, but not terrible (Java does this all the time):

class Foo implements TypeAtLeastRequired<Foo> { // error
  msg: string = 'Hello!';
}

class Bar implements TypeAtLeastRequired<Bar> {
  msg: string = 'Hello!';
  okay() { }
}

If you want to do it with object literals, you will be annoyed if you have to give the type a name just to check it. Instead you could use a generic helper function to infer the type:

const typeAtLeastRequired = <T,>(t: TypeAtLeastRequired<T>) => t;

And instead of writing const v: TypeAtLeastRequired = {...}, you write const v = typeAtLeastRequired({...}). Like this:

const okay = typeAtLeastRequired({
  a: 0,
  b: () => 2
})

const notOkay = typeAtLeastRequired({
  a: 0, // error! 
  b: 2
})

Playground link to code

  • Related