最新消息:雨落星辰是一个专注网站SEO优化、网站SEO诊断、搜索引擎研究、网络营销推广、网站策划运营及站长类的自媒体原创博客

typescript - How do I only allow string properties in a class? - Stack Overflow

programmeradmin1浏览0评论

In express, the value of req.params is an object, which can only have string properties.

By default, req.params is typed to be Record<string, string>, which isn't really useful. I want to provide typing information to the compiler so that it knows which properties are expected and not expected inside req.params.

I can do it in many ways. What I ended up doing is creating an abstraction that allows me to define the shape of req.params using a class definition. This is useful because I can give this class to class-validator and have runtime validations as well for free.

This abstraction accepts the class and returns a typed request handler. Something along the lines of:

declare function createRequestHandler<Params extends object>(
  paramsConstructor: new (...args: never) => Params,
  handler: express.RequestHandler<Params>,
): express.RequestHandler<Params>
class GetUserByIdParams {
  userId!: string
}

app.get('/users/:userId', createRequestHandler(GetUserByIdParams, (req) => {
  req.params.userId; // ✅ well-typed
}))

The problem is that I can define the params class to have any properties whatsoever, not only string or derived from string, but also number, boolean, a composite object, – which is, of course, not something what the runtime value of req.params can have.

class InvalidParams {
  userId!: number
}

app.get('/users/:userId', createRequestHandler(InvalidParams, (req) => {
  req.params.userId.toFixed(2); // ❌ method 'toFixed' doesn't exist, but no errors here
}))

Yes, I get validation errors from class-validator when I actually run this code, but I want to have this constraint at design time as well.

How do I restrict Params to only have string properties?

In express, the value of req.params is an object, which can only have string properties.

By default, req.params is typed to be Record<string, string>, which isn't really useful. I want to provide typing information to the compiler so that it knows which properties are expected and not expected inside req.params.

I can do it in many ways. What I ended up doing is creating an abstraction that allows me to define the shape of req.params using a class definition. This is useful because I can give this class to class-validator and have runtime validations as well for free.

This abstraction accepts the class and returns a typed request handler. Something along the lines of:

declare function createRequestHandler<Params extends object>(
  paramsConstructor: new (...args: never) => Params,
  handler: express.RequestHandler<Params>,
): express.RequestHandler<Params>
class GetUserByIdParams {
  userId!: string
}

app.get('/users/:userId', createRequestHandler(GetUserByIdParams, (req) => {
  req.params.userId; // ✅ well-typed
}))

The problem is that I can define the params class to have any properties whatsoever, not only string or derived from string, but also number, boolean, a composite object, – which is, of course, not something what the runtime value of req.params can have.

class InvalidParams {
  userId!: number
}

app.get('/users/:userId', createRequestHandler(InvalidParams, (req) => {
  req.params.userId.toFixed(2); // ❌ method 'toFixed' doesn't exist, but no errors here
}))

Yes, I get validation errors from class-validator when I actually run this code, but I want to have this constraint at design time as well.

How do I restrict Params to only have string properties?

Share Improve this question asked Feb 4 at 17:22 Parzh from UkraineParzh from Ukraine 9,9134 gold badges46 silver badges80 bronze badges
Add a comment  | 

1 Answer 1

Reset to default 0

There are two ways: 1) easy and annoying, and 2) complex and robust.

Easy and annoying: index signatures

The easy way to restrict class properties to be strings is to add an index signature:

class Params {
  [key: string]: string

  userId!: number // ✅ Error: Property 'userId' of type 'number' is not assignable to 'string' index type 'string'
}

But then again, nobody stops the developer from just not including it:

class Params {
  userId!: number // ❌ no errors
}

Index signatures can be enforced by createRequestHandler with a small change in the type argument constraint: from extends object to extends Record<string, string>:

declare function createRequestHandler<Params extends Record<string, string>>(…): …
class Params {
  userId!: number
}

createRequestHandler(Params, () => …)
//                   ^^^^^^
// ✅ Error: Index signature for type 'string' is missing in type 'Params'

This makes index signatures unavoidable. But index signatures have one other unfortunate functionality: they allow accessing unknown properties:

enum UserRole { Admin = 'Admin', User = 'User' }

class Params {
  [key: string]: string

  userId!: string
  userRole!: UserRole
}

new Params().userId // ✅ string
new Params().userRole // ✅ UserRole
new Params().fooBar // ❌ string, no error
new Params().literallyDoesNotExist // ❌ string, no error

This makes the approach just barely more useful compared to the default Record<string, string>.

Complex and robust

A more complex but much more powerful approach would be to gather the properties of the model and assess that all of them are assignable to string (meaning, the property value is either a string, or something that derives from string, like a string literal type, or an enum that resolves to string values). This would make an input class invalid if any one of its properties is something more generic than string (e.g., string | number or unknown).

The keyof operator can help to get a union of all values of all properties of an object:

type ValuesOf<Obj extends object> = Obj[keyof Obj]

Try it.

A simple type utility that checks if one type is derived from another type would look like this:

type SubType<Type, SuperType> = [Type] extends [SuperType] ? Type : never

Try it.

Combining the above, this type utility returns never if at least one property of the object is more general than string:

type ObjWithStringProps<Obj extends object> = [Obj[keyof Obj]] extends [string] ? Obj : never;

Try it.

To apply this to createRequestHandler, it is instructed to make sure that the given class constructs an instance which only has string or string-derived properties:

declare function createRequestHandler<Params extends object>(
  paramsConstructor: new (...args: never) => ObjWithStringProps<Params>,
  handler: …
): …
enum UserRole { Admin = 'Admin', User = 'User' }

class Params {
    userId!: string
    userRole!: UserRole
}

createRequestHandler(Params, (req) => {
    req.params.userId // string
    req.params.userRole // UserRole
    req.params.doesNotExist; // ✅ Error: Property 'doesNotExist' does not exist on type 'Params'
})

Try it.

发布评论

评论列表(0)

  1. 暂无评论