telegraf: Typescript: Context message does not contain text

Hello everyone, before starting this thread I would love to introduce my setup

  • Telegraf.js Version: 4.0.1
  • Node.js Version: 14
  • Operating System: OSX or Linux

The main problem is when I am using TypeScript It’s not possible to access on Text property from the message itself.

  import { Context } from 'telegraf';

   // *** [More code...] ***

  bot.on(['text', 'edited_message'], (ctx: Context) => {
    const msg = ctx.message ?? ctx.editedMessage;
    console.log(msg.text) // We will read the message text into the console
  });

This will throw a typescript error described below.

TS2339: Property 'text' does not exist on type '(New & NonChannel & TextMessage) | (New & NonChannel & DocumentMessage) | (New & NonChannel & AudioMessage) | ... 54 more ... | (Edited & ... 1 more ... & VoiceMessage)'.   Property 'text' does not exist on type 'New & NonChannel & DocumentMessage'.

And this is my tsconfig.json

{
  "$schema": "https://json.schemastore.org/tsconfig",
  "display": "Node 14",
  "compilerOptions": {
    "module": "commonjs",
    "lib": ["es2020"],
    "esModuleInterop": true,
    "removeComments": true,
    "allowSyntheticDefaultImports": true,
    "target": "es6",
    "noImplicitAny": true,
    "moduleResolution": "node",
    "sourceMap": true,
    "outDir": "build",
    "baseUrl": ".",
    "experimentalDecorators": true,
    "emitDecoratorMetadata": true,
    "skipLibCheck": true,
    "forceConsistentCasingInFileNames": true,
    "paths": {
      "*": [
        "node_modules/*",
        "src/types/*"
      ]
    }
  },
  "include": [
    "src/**/*"
  ],
  "exclude": [
    "__tests__"
  ]
}

About this issue

  • Original URL
  • State: closed
  • Created 3 years ago
  • Comments: 20 (10 by maintainers)

Commits related to this issue

Most upvoted comments

Preferably, use .on('text'). If there’s no updateType you want, use .guard, or narrow down manually:

// 4.0
if (ctx.message === undefined) return next()
if(!('text' in ctx.message)) return next()
// 4.1 (hopefully)
if(!is.text(ctx.message)) return next()

Then TypeScript knows that ctx.message.text is present, and thus for example ctx.message.photo must be absent.

// 3.38 would require non-null assertions
bot.on('text', (ctx) => {
  console.log(ctx.message!.text!.toLowerCase())
})

Would this satisfy you?

// 4.1 hopefully
bot.use(ctx => {
  console.log(deunionize(ctx.message)?.text.toLowerCase())
})

I have 2 implementations already, sadly neither works on Chat, I can work on that.

I don’t dislike it. I’m just unable to allow it without requiring non-null assertions everywhere else. PR welcome.

https://github.com/tdlib/telegram-bot-api/issues/93 would allow

if (ctx.message?.type !== 'text') return next()

Not perfect, but worked well on everything I threw at it:

type PropOr<T, P extends string | symbol | number, D> = T extends Partial<Record<P, infer V>> ? V : D

type UnionKeys<T> = T extends unknown ? keyof T : never

type Deunionize<T> = ([undefined] extends [T] ? undefined : never) | {
  [K in UnionKeys<T>]: PropOr<NonNullable<T>, K, undefined>
}

function deunionize<T extends object | undefined>(t: T) {
  return t as Deunionize<T>
}

I’ll ship it in 4.1 so that people can improve it via pull requests.

It’s good that we can have it, but it still feels a bit ‘hacky’. I’ll spend some time and check typings in repo, but if it’s impossible to avoid non-null assertions, then your solution looks good

I still can’t get what is the problem to always have a text in message? You can use 3.38 version without non-null assertions:

bot.use((ctx) => {
  console.log(ctx.message?.text) // or ctx.message?.text ?? '' if you need to pass a string
})

It looks like you’re trying to remove possible fields form type, but these fields are exist it this type, based on telegram-typings and on telegram official docs.

Also based on typescript handbook there is no any sense to remove optional properties from interfaces. There is makes sense to clarify types like this:

bot.use((ctx: TextContext) => {
  console.log(ctx.message.text) // now you don't need to use optional chaining
})

// or

bot.on('text', (ctx) => {  // use TextContext by default for bot.text
  console.log(ctx.message.text)
})

My point here is that maybe you should consider not removing optional fields from interfaces and fix nullability with more specific contexts interfaces.

Not every edited message is edited text message (#1218).

For now, narrow it down manually with if (!("text" in msg)) return next() or use bot.guard.