diff --git a/package.json b/package.json
index 514da0c7cc..0a28fea05b 100644
--- a/package.json
+++ b/package.json
@@ -209,6 +209,7 @@
 		"seedrandom": "3.0.5",
 		"sharp": "0.29.1",
 		"speakeasy": "2.0.0",
+		"strict-event-emitter-types": "2.0.0",
 		"stringz": "2.1.0",
 		"style-loader": "3.2.1",
 		"summaly": "2.4.1",
diff --git a/src/misc/check-hit-antenna.ts b/src/misc/check-hit-antenna.ts
index 38965f4b0d..3789054b26 100644
--- a/src/misc/check-hit-antenna.ts
+++ b/src/misc/check-hit-antenna.ts
@@ -3,13 +3,13 @@ import { Note } from '@/models/entities/note';
 import { User } from '@/models/entities/user';
 import { UserListJoinings, UserGroupJoinings } from '@/models/index';
 import { getFullApAccount } from './convert-host';
-import { PackedNote } from '../models/repositories/note';
 import { parseAcct } from '@/misc/acct';
+import { Packed } from './schema';
 
 /**
  * noteUserFollowers / antennaUserFollowing はどちらか一方が指定されていればよい
  */
-export async function checkHitAntenna(antenna: Antenna, note: (Note | PackedNote), noteUser: { username: string; host: string | null; }, noteUserFollowers?: User['id'][], antennaUserFollowing?: User['id'][]): Promise<boolean> {
+export async function checkHitAntenna(antenna: Antenna, note: (Note | Packed<'Note'>), noteUser: { username: string; host: string | null; }, noteUserFollowers?: User['id'][], antennaUserFollowing?: User['id'][]): Promise<boolean> {
 	if (note.visibility === 'specified') return false;
 
 	if (note.visibility === 'followers') {
diff --git a/src/misc/schema.ts b/src/misc/schema.ts
index d27c9eff99..4131875ef7 100644
--- a/src/misc/schema.ts
+++ b/src/misc/schema.ts
@@ -21,6 +21,9 @@ import { packedClipSchema } from '@/models/repositories/clip';
 import { packedFederationInstanceSchema } from '@/models/repositories/federation-instance';
 import { packedQueueCountSchema } from '@/models/repositories/queue';
 import { packedGalleryPostSchema } from '@/models/repositories/gallery-post';
+import { packedEmojiSchema } from '@/models/repositories/emoji';
+import { packedReversiGameSchema } from '@/models/repositories/games/reversi/game';
+import { packedReversiMatchingSchema } from '@/models/repositories/games/reversi/matching';
 
 export const refs = {
 	User: packedUserSchema,
@@ -45,8 +48,13 @@ export const refs = {
 	Clip: packedClipSchema,
 	FederationInstance: packedFederationInstanceSchema,
 	GalleryPost: packedGalleryPostSchema,
+	Emoji: packedEmojiSchema,
+	ReversiGame: packedReversiGameSchema,
+	ReversiMatching: packedReversiMatchingSchema,
 };
 
+export type Packed<x extends keyof typeof refs> = ObjType<(typeof refs[x])['properties']>;
+
 export interface Schema extends SimpleSchema {
 	items?: Schema;
 	properties?: Obj;
@@ -92,7 +100,7 @@ export type SchemaType<p extends Schema> =
 	p['type'] extends 'array' ? NullOrUndefined<p, MyType<NonNullable<p['items']>>[]> :
 	p['type'] extends 'object' ? (
 		p['ref'] extends keyof typeof refs
-			? NullOrUndefined<p, SchemaType<typeof refs[p['ref']]>>
+			? NullOrUndefined<p, Packed<p['ref']>>
 			: NullOrUndefined<p, ObjType<NonNullable<p['properties']>>>
 	) :
 	p['type'] extends 'any' ? NullOrUndefined<p, any> :
diff --git a/src/models/repositories/antenna.ts b/src/models/repositories/antenna.ts
index e61eed5e08..657de55581 100644
--- a/src/models/repositories/antenna.ts
+++ b/src/models/repositories/antenna.ts
@@ -1,15 +1,13 @@
 import { EntityRepository, Repository } from 'typeorm';
 import { Antenna } from '@/models/entities/antenna';
-import { SchemaType } from '@/misc/schema';
+import { Packed } from '@/misc/schema';
 import { AntennaNotes, UserGroupJoinings } from '../index';
 
-export type PackedAntenna = SchemaType<typeof packedAntennaSchema>;
-
 @EntityRepository(Antenna)
 export class AntennaRepository extends Repository<Antenna> {
 	public async pack(
 		src: Antenna['id'] | Antenna,
-	): Promise<PackedAntenna> {
+	): Promise<Packed<'Antenna'>> {
 		const antenna = typeof src === 'object' ? src : await this.findOneOrFail(src);
 
 		const hasUnreadNote = (await AntennaNotes.findOne({ antennaId: antenna.id, read: false })) != null;
diff --git a/src/models/repositories/app.ts b/src/models/repositories/app.ts
index 2287bd4390..0226edad11 100644
--- a/src/models/repositories/app.ts
+++ b/src/models/repositories/app.ts
@@ -1,9 +1,8 @@
 import { EntityRepository, Repository } from 'typeorm';
 import { App } from '@/models/entities/app';
 import { AccessTokens } from '../index';
-import { SchemaType } from '@/misc/schema';
-
-export type PackedApp = SchemaType<typeof packedAppSchema>;
+import { Packed } from '@/misc/schema';
+import { User } from '../entities/user';
 
 @EntityRepository(App)
 export class AppRepository extends Repository<App> {
@@ -15,7 +14,7 @@ export class AppRepository extends Repository<App> {
 			includeSecret?: boolean,
 			includeProfileImageIds?: boolean
 		}
-	): Promise<PackedApp> {
+	): Promise<Packed<'App'>> {
 		const opts = Object.assign({
 			detail: false,
 			includeSecret: false,
@@ -52,13 +51,9 @@ export const packedAppSchema = {
 			type: 'string' as const,
 			optional: false as const, nullable: false as const
 		},
-		createdAt: {
+		callbackUrl: {
 			type: 'string' as const,
-			optional: false as const, nullable: false as const
-		},
-		lastUsedAt: {
-			type: 'string' as const,
-			optional: false as const, nullable: false as const
+			optional: false as const, nullable: true as const
 		},
 		permission: {
 			type: 'array' as const,
diff --git a/src/models/repositories/blocking.ts b/src/models/repositories/blocking.ts
index 515b3a6b16..ac60c9a4ce 100644
--- a/src/models/repositories/blocking.ts
+++ b/src/models/repositories/blocking.ts
@@ -2,17 +2,15 @@ import { EntityRepository, Repository } from 'typeorm';
 import { Users } from '../index';
 import { Blocking } from '@/models/entities/blocking';
 import { awaitAll } from '@/prelude/await-all';
-import { SchemaType } from '@/misc/schema';
+import { Packed } from '@/misc/schema';
 import { User } from '@/models/entities/user';
 
-export type PackedBlocking = SchemaType<typeof packedBlockingSchema>;
-
 @EntityRepository(Blocking)
 export class BlockingRepository extends Repository<Blocking> {
 	public async pack(
 		src: Blocking['id'] | Blocking,
 		me?: { id: User['id'] } | null | undefined
-	): Promise<PackedBlocking> {
+	): Promise<Packed<'Blocking'>> {
 		const blocking = typeof src === 'object' ? src : await this.findOneOrFail(src);
 
 		return await awaitAll({
diff --git a/src/models/repositories/channel.ts b/src/models/repositories/channel.ts
index 4bb829f570..5c7d095473 100644
--- a/src/models/repositories/channel.ts
+++ b/src/models/repositories/channel.ts
@@ -1,17 +1,15 @@
 import { EntityRepository, Repository } from 'typeorm';
 import { Channel } from '@/models/entities/channel';
-import { SchemaType } from '@/misc/schema';
+import { Packed } from '@/misc/schema';
 import { DriveFiles, ChannelFollowings, NoteUnreads } from '../index';
 import { User } from '@/models/entities/user';
 
-export type PackedChannel = SchemaType<typeof packedChannelSchema>;
-
 @EntityRepository(Channel)
 export class ChannelRepository extends Repository<Channel> {
 	public async pack(
 		src: Channel['id'] | Channel,
 		me?: { id: User['id'] } | null | undefined,
-	): Promise<PackedChannel> {
+	): Promise<Packed<'Channel'>> {
 		const channel = typeof src === 'object' ? src : await this.findOneOrFail(src);
 		const meId = me ? me.id : null;
 
diff --git a/src/models/repositories/clip.ts b/src/models/repositories/clip.ts
index e3d718bef4..7892811d48 100644
--- a/src/models/repositories/clip.ts
+++ b/src/models/repositories/clip.ts
@@ -1,16 +1,14 @@
 import { EntityRepository, Repository } from 'typeorm';
 import { Clip } from '@/models/entities/clip';
-import { SchemaType } from '@/misc/schema';
+import { Packed } from '@/misc/schema';
 import { Users } from '../index';
 import { awaitAll } from '@/prelude/await-all';
 
-export type PackedClip = SchemaType<typeof packedClipSchema>;
-
 @EntityRepository(Clip)
 export class ClipRepository extends Repository<Clip> {
 	public async pack(
 		src: Clip['id'] | Clip,
-	): Promise<PackedClip> {
+	): Promise<Packed<'Clip'>> {
 		const clip = typeof src === 'object' ? src : await this.findOneOrFail(src);
 
 		return await awaitAll({
diff --git a/src/models/repositories/drive-file.ts b/src/models/repositories/drive-file.ts
index 63bd020cbe..ddf9a46afd 100644
--- a/src/models/repositories/drive-file.ts
+++ b/src/models/repositories/drive-file.ts
@@ -4,14 +4,12 @@ import { Users, DriveFolders } from '../index';
 import { User } from '@/models/entities/user';
 import { toPuny } from '@/misc/convert-host';
 import { awaitAll } from '@/prelude/await-all';
-import { SchemaType } from '@/misc/schema';
+import { Packed } from '@/misc/schema';
 import config from '@/config/index';
 import { query, appendQuery } from '@/prelude/url';
 import { Meta } from '@/models/entities/meta';
 import { fetchMeta } from '@/misc/fetch-meta';
 
-export type PackedDriveFile = SchemaType<typeof packedDriveFileSchema>;
-
 type PackOptions = {
 	detail?: boolean,
 	self?: boolean,
@@ -99,12 +97,12 @@ export class DriveFileRepository extends Repository<DriveFile> {
 		return parseInt(sum, 10) || 0;
 	}
 
-	public async pack(src: DriveFile['id'], options?: PackOptions): Promise<PackedDriveFile | null>;
-	public async pack(src: DriveFile, options?: PackOptions): Promise<PackedDriveFile>;
+	public async pack(src: DriveFile['id'], options?: PackOptions): Promise<Packed<'DriveFile'> | null>;
+	public async pack(src: DriveFile, options?: PackOptions): Promise<Packed<'DriveFile'>>;
 	public async pack(
 		src: DriveFile['id'] | DriveFile,
 		options?: PackOptions
-	): Promise<PackedDriveFile | null> {
+	): Promise<Packed<'DriveFile'> | null> {
 		const opts = Object.assign({
 			detail: false,
 			self: false
diff --git a/src/models/repositories/drive-folder.ts b/src/models/repositories/drive-folder.ts
index bc73018f29..8ef6f01b5d 100644
--- a/src/models/repositories/drive-folder.ts
+++ b/src/models/repositories/drive-folder.ts
@@ -2,9 +2,7 @@ import { EntityRepository, Repository } from 'typeorm';
 import { DriveFolders, DriveFiles } from '../index';
 import { DriveFolder } from '@/models/entities/drive-folder';
 import { awaitAll } from '@/prelude/await-all';
-import { SchemaType } from '@/misc/schema';
-
-export type PackedDriveFolder = SchemaType<typeof packedDriveFolderSchema>;
+import { Packed } from '@/misc/schema';
 
 @EntityRepository(DriveFolder)
 export class DriveFolderRepository extends Repository<DriveFolder> {
@@ -20,7 +18,7 @@ export class DriveFolderRepository extends Repository<DriveFolder> {
 		options?: {
 			detail: boolean
 		}
-	): Promise<PackedDriveFolder> {
+	): Promise<Packed<'DriveFolder'>> {
 		const opts = Object.assign({
 			detail: false
 		}, options);
diff --git a/src/models/repositories/emoji.ts b/src/models/repositories/emoji.ts
index c3d7184ec9..7985c27aba 100644
--- a/src/models/repositories/emoji.ts
+++ b/src/models/repositories/emoji.ts
@@ -1,11 +1,12 @@
 import { EntityRepository, Repository } from 'typeorm';
 import { Emoji } from '@/models/entities/emoji';
+import { Packed } from '@/misc/schema';
 
 @EntityRepository(Emoji)
 export class EmojiRepository extends Repository<Emoji> {
 	public async pack(
 		src: Emoji['id'] | Emoji,
-	) {
+	): Promise<Packed<'Emoji'>> {
 		const emoji = typeof src === 'object' ? src : await this.findOneOrFail(src);
 
 		return {
@@ -24,3 +25,41 @@ export class EmojiRepository extends Repository<Emoji> {
 		return Promise.all(emojis.map(x => this.pack(x)));
 	}
 }
+
+export const packedEmojiSchema = {
+	type: 'object' as const,
+	optional: false as const, nullable: false as const,
+	properties: {
+		id: {
+			type: 'string' as const,
+			optional: false as const, nullable: false as const,
+			format: 'id',
+			example: 'xxxxxxxxxx',
+		},
+		aliases: {
+			type: 'array' as const,
+			optional: false as const, nullable: false as const,
+			items: {
+				type: 'string' as const,
+				optional: false as const, nullable: false as const,
+				format: 'id',
+			},
+		},
+		name: {
+			type: 'string' as const,
+			optional: false as const, nullable: false as const,
+		},
+		category: {
+			type: 'string' as const,
+			optional: false as const, nullable: true as const,
+		},
+		host: {
+			type: 'string' as const,
+			optional: false as const, nullable: true as const,
+		},
+		url: {
+			type: 'string' as const,
+			optional: false as const, nullable: false as const,
+		},
+	}
+};
diff --git a/src/models/repositories/following.ts b/src/models/repositories/following.ts
index 24ddd0d676..b1f716069f 100644
--- a/src/models/repositories/following.ts
+++ b/src/models/repositories/following.ts
@@ -2,7 +2,7 @@ import { EntityRepository, Repository } from 'typeorm';
 import { Users } from '../index';
 import { Following } from '@/models/entities/following';
 import { awaitAll } from '@/prelude/await-all';
-import { SchemaType } from '@/misc/schema';
+import { Packed } from '@/misc/schema';
 import { User } from '@/models/entities/user';
 
 type LocalFollowerFollowing = Following & {
@@ -29,8 +29,6 @@ type RemoteFolloweeFollowing = Following & {
 	followeeSharedInbox: string;
 };
 
-export type PackedFollowing = SchemaType<typeof packedFollowingSchema>;
-
 @EntityRepository(Following)
 export class FollowingRepository extends Repository<Following> {
 	public isLocalFollower(following: Following): following is LocalFollowerFollowing {
@@ -56,7 +54,7 @@ export class FollowingRepository extends Repository<Following> {
 			populateFollowee?: boolean;
 			populateFollower?: boolean;
 		}
-	): Promise<PackedFollowing> {
+	): Promise<Packed<'Following'>> {
 		const following = typeof src === 'object' ? src : await this.findOneOrFail(src);
 
 		if (opts == null) opts = {};
diff --git a/src/models/repositories/gallery-post.ts b/src/models/repositories/gallery-post.ts
index afa22e9edf..4f666ff252 100644
--- a/src/models/repositories/gallery-post.ts
+++ b/src/models/repositories/gallery-post.ts
@@ -1,18 +1,16 @@
 import { EntityRepository, Repository } from 'typeorm';
 import { GalleryPost } from '@/models/entities/gallery-post';
-import { SchemaType } from '@/misc/schema';
+import { Packed } from '@/misc/schema';
 import { Users, DriveFiles, GalleryLikes } from '../index';
 import { awaitAll } from '@/prelude/await-all';
 import { User } from '@/models/entities/user';
 
-export type PackedGalleryPost = SchemaType<typeof packedGalleryPostSchema>;
-
 @EntityRepository(GalleryPost)
 export class GalleryPostRepository extends Repository<GalleryPost> {
 	public async pack(
 		src: GalleryPost['id'] | GalleryPost,
 		me?: { id: User['id'] } | null | undefined,
-	): Promise<PackedGalleryPost> {
+	): Promise<Packed<'GalleryPost'>> {
 		const meId = me ? me.id : null;
 		const post = typeof src === 'object' ? src : await this.findOneOrFail(src);
 
diff --git a/src/models/repositories/games/reversi/game.ts b/src/models/repositories/games/reversi/game.ts
index dc91ad51b8..9adb386fa9 100644
--- a/src/models/repositories/games/reversi/game.ts
+++ b/src/models/repositories/games/reversi/game.ts
@@ -2,6 +2,7 @@ import { User } from '@/models/entities/user';
 import { EntityRepository, Repository } from 'typeorm';
 import { Users } from '../../../index';
 import { ReversiGame } from '@/models/entities/games/reversi/game';
+import { Packed } from '@/misc/schema';
 
 @EntityRepository(ReversiGame)
 export class ReversiGameRepository extends Repository<ReversiGame> {
@@ -11,7 +12,7 @@ export class ReversiGameRepository extends Repository<ReversiGame> {
 		options?: {
 			detail?: boolean
 		}
-	) {
+	): Promise<Packed<'ReversiGame'>> {
 		const opts = Object.assign({
 			detail: true
 		}, options);
@@ -20,8 +21,8 @@ export class ReversiGameRepository extends Repository<ReversiGame> {
 
 		return {
 			id: game.id,
-			createdAt: game.createdAt,
-			startedAt: game.startedAt,
+			createdAt: game.createdAt.toISOString(),
+			startedAt: game.startedAt && game.startedAt.toISOString(),
 			isStarted: game.isStarted,
 			isEnded: game.isEnded,
 			form1: game.form1,
@@ -41,9 +42,150 @@ export class ReversiGameRepository extends Repository<ReversiGame> {
 			canPutEverywhere: game.canPutEverywhere,
 			loopedBoard: game.loopedBoard,
 			...(opts.detail ? {
-				logs: game.logs,
+				logs: game.logs.map(log => ({
+					at: log.at.toISOString(),
+					color: log.color,
+					pos: log.pos
+				})),
 				map: game.map,
 			} : {})
 		};
 	}
 }
+
+export const packedReversiGameSchema = {
+	type: 'object' as const,
+	optional: false as const, nullable: false as const,
+	properties: {
+		id: {
+			type: 'string' as const,
+			optional: false as const, nullable: false as const,
+			format: 'id',
+			example: 'xxxxxxxxxx',
+		},
+		createdAt: {
+			type: 'string' as const,
+			optional: false as const, nullable: false as const,
+			format: 'date-time',
+		},
+		startedAt: {
+			type: 'string' as const,
+			optional: false as const, nullable: true as const,
+			format: 'date-time',
+		},
+		isStarted: {
+			type: 'boolean' as const,
+			optional: false as const, nullable: false as const,
+		},
+		isEnded: {
+			type: 'boolean' as const,
+			optional: false as const, nullable: false as const,
+		},
+		form1: {
+			type: 'any' as const,
+			optional: false as const, nullable: true as const,
+		},
+		form2: {
+			type: 'any' as const,
+			optional: false as const, nullable: true as const,
+		},
+		user1Accepted: {
+			type: 'boolean' as const,
+			optional: false as const, nullable: false as const,
+		},
+		user2Accepted: {
+			type: 'boolean' as const,
+			optional: false as const, nullable: false as const,
+		},
+		user1Id: {
+			type: 'string' as const,
+			optional: false as const, nullable: false as const,
+			format: 'id',
+			example: 'xxxxxxxxxx',
+		},
+		user2Id: {
+			type: 'string' as const,
+			optional: false as const, nullable: false as const,
+			format: 'id',
+			example: 'xxxxxxxxxx',
+		},
+		user1: {
+			type: 'object' as const,
+			optional: false as const, nullable: false as const,
+			ref: 'User' as const,
+		},
+		user2: {
+			type: 'object' as const,
+			optional: false as const, nullable: false as const,
+			ref: 'User' as const,
+		},
+		winnerId: {
+			type: 'string' as const,
+			optional: false as const, nullable: true as const,
+			format: 'id',
+			example: 'xxxxxxxxxx',
+		},
+		winner: {
+			type: 'object' as const,
+			optional: false as const, nullable: true as const,
+			ref: 'User' as const,
+		},
+		surrendered: {
+			type: 'string' as const,
+			optional: false as const, nullable: true as const,
+			format: 'id',
+			example: 'xxxxxxxxxx',
+		},
+		black: {
+			type: 'number' as const,
+			optional: false as const, nullable: true as const,
+		},
+		bw: {
+			type: 'string' as const,
+			optional: false as const, nullable: false as const,
+		},
+		isLlotheo: {
+			type: 'boolean' as const,
+			optional: false as const, nullable: false as const,
+		},
+		canPutEverywhere: {
+			type: 'boolean' as const,
+			optional: false as const, nullable: false as const,
+		},
+		loopedBoard: {
+			type: 'boolean' as const,
+			optional: false as const, nullable: false as const,
+		},
+		logs: {
+			type: 'array' as const,
+			optional: true as const, nullable: false as const,
+			items: {
+				type: 'object' as const,
+				optional: true as const, nullable: false as const,
+				properties: {
+					at: {
+						type: 'string' as const,
+						optional: false as const, nullable: false as const,
+						format: 'date-time',
+					},
+					color: {
+						type: 'boolean' as const,
+						optional: false as const, nullable: false as const,
+					},
+					pos: {
+						type: 'number' as const,
+						optional: false as const, nullable: false as const,
+					},
+				}
+			}
+		},
+		map: {
+			type: 'array' as const,
+			optional: true as const, nullable: false as const,
+			items: {
+				type: 'string' as const,
+				optional: false as const, nullable: false as const,
+			}
+		}
+	}
+};
diff --git a/src/models/repositories/games/reversi/matching.ts b/src/models/repositories/games/reversi/matching.ts
index 148221dee5..b4515800df 100644
--- a/src/models/repositories/games/reversi/matching.ts
+++ b/src/models/repositories/games/reversi/matching.ts
@@ -3,18 +3,19 @@ import { ReversiMatching } from '@/models/entities/games/reversi/matching';
 import { Users } from '../../../index';
 import { awaitAll } from '@/prelude/await-all';
 import { User } from '@/models/entities/user';
+import { Packed } from '@/misc/schema';
 
 @EntityRepository(ReversiMatching)
 export class ReversiMatchingRepository extends Repository<ReversiMatching> {
 	public async pack(
 		src: ReversiMatching['id'] | ReversiMatching,
 		me: { id: User['id'] }
-	) {
+	): Promise<Packed<'ReversiMatching'>> {
 		const matching = typeof src === 'object' ? src : await this.findOneOrFail(src);
 
 		return await awaitAll({
 			id: matching.id,
-			createdAt: matching.createdAt,
+			createdAt: matching.createdAt.toISOString(),
 			parentId: matching.parentId,
 			parent: Users.pack(matching.parentId, me, {
 				detail: true
@@ -26,3 +27,43 @@ export class ReversiMatchingRepository extends Repository<ReversiMatching> {
 		});
 	}
 }
+
+export const packedReversiMatchingSchema = {
+	type: 'object' as const,
+	optional: false as const, nullable: false as const,
+	properties: {
+		id: {
+			type: 'string' as const,
+			optional: false as const, nullable: false as const,
+			format: 'id',
+			example: 'xxxxxxxxxx',
+		},
+		createdAt: {
+			type: 'string' as const,
+			optional: false as const, nullable: false as const,
+			format: 'date-time',
+		},
+		parentId: {
+			type: 'string' as const,
+			optional: false as const, nullable: false as const,
+			format: 'id',
+			example: 'xxxxxxxxxx',
+		},
+		parent: {
+			type: 'object' as const,
+			optional: false as const, nullable: true as const,
+			ref: 'User' as const,
+		},
+		childId: {
+			type: 'string' as const,
+			optional: false as const, nullable: false as const,
+			format: 'id',
+			example: 'xxxxxxxxxx',
+		},
+		child: {
+			type: 'object' as const,
+			optional: false as const, nullable: false as const,
+			ref: 'User' as const,
+		},
+	}
+};
diff --git a/src/models/repositories/hashtag.ts b/src/models/repositories/hashtag.ts
index ee42ad16b6..d52f6ba7c6 100644
--- a/src/models/repositories/hashtag.ts
+++ b/src/models/repositories/hashtag.ts
@@ -1,14 +1,12 @@
 import { EntityRepository, Repository } from 'typeorm';
 import { Hashtag } from '@/models/entities/hashtag';
-import { SchemaType } from '@/misc/schema';
-
-export type PackedHashtag = SchemaType<typeof packedHashtagSchema>;
+import { Packed } from '@/misc/schema';
 
 @EntityRepository(Hashtag)
 export class HashtagRepository extends Repository<Hashtag> {
 	public async pack(
 		src: Hashtag,
-	): Promise<PackedHashtag> {
+	): Promise<Packed<'Hashtag'>> {
 		return {
 			tag: src.name,
 			mentionedUsersCount: src.mentionedUsersCount,
diff --git a/src/models/repositories/messaging-message.ts b/src/models/repositories/messaging-message.ts
index f97905af2f..abdff63689 100644
--- a/src/models/repositories/messaging-message.ts
+++ b/src/models/repositories/messaging-message.ts
@@ -1,11 +1,9 @@
 import { EntityRepository, Repository } from 'typeorm';
 import { MessagingMessage } from '@/models/entities/messaging-message';
 import { Users, DriveFiles, UserGroups } from '../index';
-import { SchemaType } from '@/misc/schema';
+import { Packed } from '@/misc/schema';
 import { User } from '@/models/entities/user';
 
-export type PackedMessagingMessage = SchemaType<typeof packedMessagingMessageSchema>;
-
 @EntityRepository(MessagingMessage)
 export class MessagingMessageRepository extends Repository<MessagingMessage> {
 	public validateText(text: string): boolean {
@@ -19,7 +17,7 @@ export class MessagingMessageRepository extends Repository<MessagingMessage> {
 			populateRecipient?: boolean,
 			populateGroup?: boolean,
 		}
-	): Promise<PackedMessagingMessage> {
+	): Promise<Packed<'MessagingMessage'>> {
 		const opts = options || {
 			populateRecipient: true,
 			populateGroup: true,
diff --git a/src/models/repositories/muting.ts b/src/models/repositories/muting.ts
index d957b1792d..869afd3c4e 100644
--- a/src/models/repositories/muting.ts
+++ b/src/models/repositories/muting.ts
@@ -2,17 +2,15 @@ import { EntityRepository, Repository } from 'typeorm';
 import { Users } from '../index';
 import { Muting } from '@/models/entities/muting';
 import { awaitAll } from '@/prelude/await-all';
-import { SchemaType } from '@/misc/schema';
+import { Packed } from '@/misc/schema';
 import { User } from '@/models/entities/user';
 
-export type PackedMuting = SchemaType<typeof packedMutingSchema>;
-
 @EntityRepository(Muting)
 export class MutingRepository extends Repository<Muting> {
 	public async pack(
 		src: Muting['id'] | Muting,
 		me?: { id: User['id'] } | null | undefined
-	): Promise<PackedMuting> {
+	): Promise<Packed<'Muting'>> {
 		const muting = typeof src === 'object' ? src : await this.findOneOrFail(src);
 
 		return await awaitAll({
diff --git a/src/models/repositories/note-reaction.ts b/src/models/repositories/note-reaction.ts
index e73a832109..ba74076f6c 100644
--- a/src/models/repositories/note-reaction.ts
+++ b/src/models/repositories/note-reaction.ts
@@ -1,18 +1,16 @@
 import { EntityRepository, Repository } from 'typeorm';
 import { NoteReaction } from '@/models/entities/note-reaction';
 import { Users } from '../index';
-import { SchemaType } from '@/misc/schema';
+import { Packed } from '@/misc/schema';
 import { convertLegacyReaction } from '@/misc/reaction-lib';
 import { User } from '@/models/entities/user';
 
-export type PackedNoteReaction = SchemaType<typeof packedNoteReactionSchema>;
-
 @EntityRepository(NoteReaction)
 export class NoteReactionRepository extends Repository<NoteReaction> {
 	public async pack(
 		src: NoteReaction['id'] | NoteReaction,
 		me?: { id: User['id'] } | null | undefined
-	): Promise<PackedNoteReaction> {
+	): Promise<Packed<'NoteReaction'>> {
 		const reaction = typeof src === 'object' ? src : await this.findOneOrFail(src);
 
 		return {
diff --git a/src/models/repositories/note.ts b/src/models/repositories/note.ts
index 376a09d0c6..c0ac22b2db 100644
--- a/src/models/repositories/note.ts
+++ b/src/models/repositories/note.ts
@@ -3,15 +3,13 @@ import * as mfm from 'mfm-js';
 import { Note } from '@/models/entities/note';
 import { User } from '@/models/entities/user';
 import { Users, PollVotes, DriveFiles, NoteReactions, Followings, Polls, Channels } from '../index';
-import { SchemaType } from '@/misc/schema';
+import { Packed } from '@/misc/schema';
 import { nyaize } from '@/misc/nyaize';
 import { awaitAll } from '@/prelude/await-all';
 import { convertLegacyReaction, convertLegacyReactions, decodeReaction } from '@/misc/reaction-lib';
 import { NoteReaction } from '@/models/entities/note-reaction';
 import { aggregateNoteEmojis, populateEmojis, prefetchEmojis } from '@/misc/populate-emojis';
 
-export type PackedNote = SchemaType<typeof packedNoteSchema>;
-
 @EntityRepository(Note)
 export class NoteRepository extends Repository<Note> {
 	public validateCw(x: string) {
@@ -67,7 +65,7 @@ export class NoteRepository extends Repository<Note> {
 		return true;
 	}
 
-	private async hideNote(packedNote: PackedNote, meId: User['id'] | null) {
+	private async hideNote(packedNote: Packed<'Note'>, meId: User['id'] | null) {
 		// TODO: isVisibleForMe を使うようにしても良さそう(型違うけど)
 		let hide = false;
 
@@ -137,7 +135,7 @@ export class NoteRepository extends Repository<Note> {
 				myReactions: Map<Note['id'], NoteReaction | null>;
 			};
 		}
-	): Promise<PackedNote> {
+	): Promise<Packed<'Note'>> {
 		const opts = Object.assign({
 			detail: true,
 			skipHide: false
diff --git a/src/models/repositories/notification.ts b/src/models/repositories/notification.ts
index b7f9e3643c..d1cf9b087e 100644
--- a/src/models/repositories/notification.ts
+++ b/src/models/repositories/notification.ts
@@ -2,15 +2,13 @@ import { EntityRepository, In, Repository } from 'typeorm';
 import { Users, Notes, UserGroupInvitations, AccessTokens, NoteReactions } from '../index';
 import { Notification } from '@/models/entities/notification';
 import { awaitAll } from '@/prelude/await-all';
-import { SchemaType } from '@/misc/schema';
+import { Packed } from '@/misc/schema';
 import { Note } from '@/models/entities/note';
 import { NoteReaction } from '@/models/entities/note-reaction';
 import { User } from '@/models/entities/user';
 import { aggregateNoteEmojis, prefetchEmojis } from '@/misc/populate-emojis';
 import { notificationTypes } from '@/types';
 
-export type PackedNotification = SchemaType<typeof packedNotificationSchema>;
-
 @EntityRepository(Notification)
 export class NotificationRepository extends Repository<Notification> {
 	public async pack(
@@ -20,7 +18,7 @@ export class NotificationRepository extends Repository<Notification> {
 				myReactions: Map<Note['id'], NoteReaction | null>;
 			};
 		}
-	): Promise<PackedNotification> {
+	): Promise<Packed<'Notification'>> {
 		const notification = typeof src === 'object' ? src : await this.findOneOrFail(src);
 		const token = notification.appAccessTokenId ? await AccessTokens.findOneOrFail(notification.appAccessTokenId) : null;
 
diff --git a/src/models/repositories/page.ts b/src/models/repositories/page.ts
index 1a61e2c99c..3a3642d7ec 100644
--- a/src/models/repositories/page.ts
+++ b/src/models/repositories/page.ts
@@ -1,19 +1,17 @@
 import { EntityRepository, Repository } from 'typeorm';
 import { Page } from '@/models/entities/page';
-import { SchemaType } from '@/misc/schema';
+import { Packed } from '@/misc/schema';
 import { Users, DriveFiles, PageLikes } from '../index';
 import { awaitAll } from '@/prelude/await-all';
 import { DriveFile } from '@/models/entities/drive-file';
 import { User } from '@/models/entities/user';
 
-export type PackedPage = SchemaType<typeof packedPageSchema>;
-
 @EntityRepository(Page)
 export class PageRepository extends Repository<Page> {
 	public async pack(
 		src: Page['id'] | Page,
 		me?: { id: User['id'] } | null | undefined,
-	): Promise<PackedPage> {
+	): Promise<Packed<'Page'>> {
 		const meId = me ? me.id : null;
 		const page = typeof src === 'object' ? src : await this.findOneOrFail(src);
 
diff --git a/src/models/repositories/signin.ts b/src/models/repositories/signin.ts
index 9942d2d962..f375f9b5c0 100644
--- a/src/models/repositories/signin.ts
+++ b/src/models/repositories/signin.ts
@@ -4,7 +4,7 @@ import { Signin } from '@/models/entities/signin';
 @EntityRepository(Signin)
 export class SigninRepository extends Repository<Signin> {
 	public async pack(
-		src: any,
+		src: Signin,
 	) {
 		return src;
 	}
diff --git a/src/models/repositories/user-group.ts b/src/models/repositories/user-group.ts
index a76ac7b9d3..b38a2fb50d 100644
--- a/src/models/repositories/user-group.ts
+++ b/src/models/repositories/user-group.ts
@@ -1,15 +1,13 @@
 import { EntityRepository, Repository } from 'typeorm';
 import { UserGroup } from '@/models/entities/user-group';
 import { UserGroupJoinings } from '../index';
-import { SchemaType } from '@/misc/schema';
-
-export type PackedUserGroup = SchemaType<typeof packedUserGroupSchema>;
+import { Packed } from '@/misc/schema';
 
 @EntityRepository(UserGroup)
 export class UserGroupRepository extends Repository<UserGroup> {
 	public async pack(
 		src: UserGroup['id'] | UserGroup,
-	): Promise<PackedUserGroup> {
+	): Promise<Packed<'UserGroup'>> {
 		const userGroup = typeof src === 'object' ? src : await this.findOneOrFail(src);
 
 		const users = await UserGroupJoinings.find({
diff --git a/src/models/repositories/user-list.ts b/src/models/repositories/user-list.ts
index 809dbe0268..331c278e6f 100644
--- a/src/models/repositories/user-list.ts
+++ b/src/models/repositories/user-list.ts
@@ -1,15 +1,13 @@
 import { EntityRepository, Repository } from 'typeorm';
 import { UserList } from '@/models/entities/user-list';
 import { UserListJoinings } from '../index';
-import { SchemaType } from '@/misc/schema';
-
-export type PackedUserList = SchemaType<typeof packedUserListSchema>;
+import { Packed } from '@/misc/schema';
 
 @EntityRepository(UserList)
 export class UserListRepository extends Repository<UserList> {
 	public async pack(
 		src: UserList['id'] | UserList,
-	): Promise<PackedUserList> {
+	): Promise<Packed<'UserList'>> {
 		const userList = typeof src === 'object' ? src : await this.findOneOrFail(src);
 
 		const users = await UserListJoinings.find({
diff --git a/src/models/repositories/user.ts b/src/models/repositories/user.ts
index 39c90cf5ed..b6f27e32e2 100644
--- a/src/models/repositories/user.ts
+++ b/src/models/repositories/user.ts
@@ -3,14 +3,12 @@ import { EntityRepository, Repository, In, Not } from 'typeorm';
 import { User, ILocalUser, IRemoteUser } from '@/models/entities/user';
 import { Notes, NoteUnreads, FollowRequests, Notifications, MessagingMessages, UserNotePinings, Followings, Blockings, Mutings, UserProfiles, UserSecurityKeys, UserGroupJoinings, Pages, Announcements, AnnouncementReads, Antennas, AntennaNotes, ChannelFollowings, Instances } from '../index';
 import config from '@/config/index';
-import { SchemaType } from '@/misc/schema';
+import { Packed } from '@/misc/schema';
 import { awaitAll } from '@/prelude/await-all';
 import { populateEmojis } from '@/misc/populate-emojis';
 import { getAntennas } from '@/misc/antenna-cache';
 import { USER_ACTIVE_THRESHOLD, USER_ONLINE_THRESHOLD } from '@/const';
 
-export type PackedUser = SchemaType<typeof packedUserSchema>;
-
 @EntityRepository(User)
 export class UserRepository extends Repository<User> {
 	public async getRelation(me: User['id'], target: User['id']) {
@@ -164,7 +162,7 @@ export class UserRepository extends Repository<User> {
 			detail?: boolean,
 			includeSecrets?: boolean,
 		}
-	): Promise<PackedUser> {
+	): Promise<Packed<'User'>> {
 		const opts = Object.assign({
 			detail: false,
 			includeSecrets: false
diff --git a/src/server/api/common/read-messaging-message.ts b/src/server/api/common/read-messaging-message.ts
index a80a22ea99..35ff4e4693 100644
--- a/src/server/api/common/read-messaging-message.ts
+++ b/src/server/api/common/read-messaging-message.ts
@@ -93,7 +93,7 @@ export async function readGroupMessagingMessage(
 		id: In(messageIds)
 	});
 
-	const reads = [];
+	const reads: MessagingMessage['id'][] = [];
 
 	for (const message of messages) {
 		if (message.userId === userId) continue;
diff --git a/src/server/api/endpoints/antennas/update.ts b/src/server/api/endpoints/antennas/update.ts
index ff13e89bcc..d69b4feee6 100644
--- a/src/server/api/endpoints/antennas/update.ts
+++ b/src/server/api/endpoints/antennas/update.ts
@@ -137,7 +137,7 @@ export default define(meta, async (ps, user) => {
 		notify: ps.notify,
 	});
 
-	publishInternalEvent('antennaUpdated', Antennas.findOneOrFail(antenna.id));
+	publishInternalEvent('antennaUpdated', await Antennas.findOneOrFail(antenna.id));
 
 	return await Antennas.pack(antenna.id);
 });
diff --git a/src/server/api/stream/channels/antenna.ts b/src/server/api/stream/channels/antenna.ts
index bf9c53c453..864e42dc66 100644
--- a/src/server/api/stream/channels/antenna.ts
+++ b/src/server/api/stream/channels/antenna.ts
@@ -3,6 +3,7 @@ import Channel from '../channel';
 import { Notes } from '@/models/index';
 import { isMutedUserRelated } from '@/misc/is-muted-user-related';
 import { isBlockerUserRelated } from '@/misc/is-blocker-user-related';
+import { StreamMessages } from '../types';
 
 export default class extends Channel {
 	public readonly chName = 'antenna';
@@ -19,11 +20,9 @@ export default class extends Channel {
 	}
 
 	@autobind
-	private async onEvent(data: any) {
-		const { type, body } = data;
-
-		if (type === 'note') {
-			const note = await Notes.pack(body.id, this.user, { detail: true });
+	private async onEvent(data: StreamMessages['antenna']['spec']) {
+		if (data.type === 'note') {
+			const note = await Notes.pack(data.body.id, this.user, { detail: true });
 
 			// 流れてきたNoteがミュートしているユーザーが関わるものだったら無視する
 			if (isMutedUserRelated(note, this.muting)) return;
@@ -33,8 +32,6 @@ export default class extends Channel {
 			this.connection.cacheNote(note);
 
 			this.send('note', note);
-		} else {
-			this.send(type, body);
 		}
 	}
 
diff --git a/src/server/api/stream/channels/channel.ts b/src/server/api/stream/channels/channel.ts
index e6a9a6c696..c97a062c42 100644
--- a/src/server/api/stream/channels/channel.ts
+++ b/src/server/api/stream/channels/channel.ts
@@ -3,8 +3,9 @@ import Channel from '../channel';
 import { Notes, Users } from '@/models/index';
 import { isMutedUserRelated } from '@/misc/is-muted-user-related';
 import { isBlockerUserRelated } from '@/misc/is-blocker-user-related';
-import { PackedNote } from '@/models/repositories/note';
 import { User } from '@/models/entities/user';
+import { StreamMessages } from '../types';
+import { Packed } from '@/misc/schema';
 
 export default class extends Channel {
 	public readonly chName = 'channel';
@@ -25,7 +26,7 @@ export default class extends Channel {
 	}
 
 	@autobind
-	private async onNote(note: PackedNote) {
+	private async onNote(note: Packed<'Note'>) {
 		if (note.channelId !== this.channelId) return;
 
 		// リプライなら再pack
@@ -52,7 +53,7 @@ export default class extends Channel {
 	}
 
 	@autobind
-	private onEvent(data: any) {
+	private onEvent(data: StreamMessages['channel']['spec']) {
 		if (data.type === 'typing') {
 			const id = data.body;
 			const begin = this.typers[id] == null;
diff --git a/src/server/api/stream/channels/global-timeline.ts b/src/server/api/stream/channels/global-timeline.ts
index 384ed61409..f5983ab472 100644
--- a/src/server/api/stream/channels/global-timeline.ts
+++ b/src/server/api/stream/channels/global-timeline.ts
@@ -3,9 +3,9 @@ import { isMutedUserRelated } from '@/misc/is-muted-user-related';
 import Channel from '../channel';
 import { fetchMeta } from '@/misc/fetch-meta';
 import { Notes } from '@/models/index';
-import { PackedNote } from '@/models/repositories/note';
 import { checkWordMute } from '@/misc/check-word-mute';
 import { isBlockerUserRelated } from '@/misc/is-blocker-user-related';
+import { Packed } from '@/misc/schema';
 
 export default class extends Channel {
 	public readonly chName = 'globalTimeline';
@@ -24,7 +24,7 @@ export default class extends Channel {
 	}
 
 	@autobind
-	private async onNote(note: PackedNote) {
+	private async onNote(note: Packed<'Note'>) {
 		if (note.visibility !== 'public') return;
 		if (note.channelId != null) return;
 
diff --git a/src/server/api/stream/channels/hashtag.ts b/src/server/api/stream/channels/hashtag.ts
index 997ab75f6d..281be4f2eb 100644
--- a/src/server/api/stream/channels/hashtag.ts
+++ b/src/server/api/stream/channels/hashtag.ts
@@ -2,9 +2,9 @@ import autobind from 'autobind-decorator';
 import { isMutedUserRelated } from '@/misc/is-muted-user-related';
 import Channel from '../channel';
 import { Notes } from '@/models/index';
-import { PackedNote } from '@/models/repositories/note';
 import { normalizeForSearch } from '@/misc/normalize-for-search';
 import { isBlockerUserRelated } from '@/misc/is-blocker-user-related';
+import { Packed } from '@/misc/schema';
 
 export default class extends Channel {
 	public readonly chName = 'hashtag';
@@ -23,7 +23,7 @@ export default class extends Channel {
 	}
 
 	@autobind
-	private async onNote(note: PackedNote) {
+	private async onNote(note: Packed<'Note'>) {
 		const noteTags = note.tags ? note.tags.map((t: string) => t.toLowerCase()) : [];
 		const matched = this.q.some(tags => tags.every(tag => noteTags.includes(normalizeForSearch(tag))));
 		if (!matched) return;
diff --git a/src/server/api/stream/channels/home-timeline.ts b/src/server/api/stream/channels/home-timeline.ts
index 0e21ab552e..52e9aec250 100644
--- a/src/server/api/stream/channels/home-timeline.ts
+++ b/src/server/api/stream/channels/home-timeline.ts
@@ -2,9 +2,9 @@ import autobind from 'autobind-decorator';
 import { isMutedUserRelated } from '@/misc/is-muted-user-related';
 import Channel from '../channel';
 import { Notes } from '@/models/index';
-import { PackedNote } from '@/models/repositories/note';
 import { checkWordMute } from '@/misc/check-word-mute';
 import { isBlockerUserRelated } from '@/misc/is-blocker-user-related';
+import { Packed } from '@/misc/schema';
 
 export default class extends Channel {
 	public readonly chName = 'homeTimeline';
@@ -18,7 +18,7 @@ export default class extends Channel {
 	}
 
 	@autobind
-	private async onNote(note: PackedNote) {
+	private async onNote(note: Packed<'Note'>) {
 		if (note.channelId) {
 			if (!this.followingChannels.has(note.channelId)) return;
 		} else {
diff --git a/src/server/api/stream/channels/hybrid-timeline.ts b/src/server/api/stream/channels/hybrid-timeline.ts
index 0b28ff616b..51f95fc0cd 100644
--- a/src/server/api/stream/channels/hybrid-timeline.ts
+++ b/src/server/api/stream/channels/hybrid-timeline.ts
@@ -3,9 +3,9 @@ import { isMutedUserRelated } from '@/misc/is-muted-user-related';
 import Channel from '../channel';
 import { fetchMeta } from '@/misc/fetch-meta';
 import { Notes } from '@/models/index';
-import { PackedNote } from '@/models/repositories/note';
 import { checkWordMute } from '@/misc/check-word-mute';
 import { isBlockerUserRelated } from '@/misc/is-blocker-user-related';
+import { Packed } from '@/misc/schema';
 
 export default class extends Channel {
 	public readonly chName = 'hybridTimeline';
@@ -22,7 +22,7 @@ export default class extends Channel {
 	}
 
 	@autobind
-	private async onNote(note: PackedNote) {
+	private async onNote(note: Packed<'Note'>) {
 		// チャンネルの投稿ではなく、自分自身の投稿 または
 		// チャンネルの投稿ではなく、その投稿のユーザーをフォローしている または
 		// チャンネルの投稿ではなく、全体公開のローカルの投稿 または
diff --git a/src/server/api/stream/channels/local-timeline.ts b/src/server/api/stream/channels/local-timeline.ts
index 20061410c4..a6166c2be2 100644
--- a/src/server/api/stream/channels/local-timeline.ts
+++ b/src/server/api/stream/channels/local-timeline.ts
@@ -3,9 +3,9 @@ import { isMutedUserRelated } from '@/misc/is-muted-user-related';
 import Channel from '../channel';
 import { fetchMeta } from '@/misc/fetch-meta';
 import { Notes } from '@/models/index';
-import { PackedNote } from '@/models/repositories/note';
 import { checkWordMute } from '@/misc/check-word-mute';
 import { isBlockerUserRelated } from '@/misc/is-blocker-user-related';
+import { Packed } from '@/misc/schema';
 
 export default class extends Channel {
 	public readonly chName = 'localTimeline';
@@ -24,7 +24,7 @@ export default class extends Channel {
 	}
 
 	@autobind
-	private async onNote(note: PackedNote) {
+	private async onNote(note: Packed<'Note'>) {
 		if (note.user.host !== null) return;
 		if (note.visibility !== 'public') return;
 		if (note.channelId != null && !this.followingChannels.has(note.channelId)) return;
diff --git a/src/server/api/stream/channels/main.ts b/src/server/api/stream/channels/main.ts
index b99cb931da..131ac30472 100644
--- a/src/server/api/stream/channels/main.ts
+++ b/src/server/api/stream/channels/main.ts
@@ -11,35 +11,33 @@ export default class extends Channel {
 	public async init(params: any) {
 		// Subscribe main stream channel
 		this.subscriber.on(`mainStream:${this.user!.id}`, async data => {
-			const { type } = data;
-			let { body } = data;
-
-			switch (type) {
+			switch (data.type) {
 				case 'notification': {
-					if (this.muting.has(body.userId)) return;
-					if (body.note && body.note.isHidden) {
-						const note = await Notes.pack(body.note.id, this.user, {
+					if (data.body.userId && this.muting.has(data.body.userId)) return;
+
+					if (data.body.note && data.body.note.isHidden) {
+						const note = await Notes.pack(data.body.note.id, this.user, {
 							detail: true
 						});
 						this.connection.cacheNote(note);
-						body.note = note;
+						data.body.note = note;
 					}
 					break;
 				}
 				case 'mention': {
-					if (this.muting.has(body.userId)) return;
-					if (body.isHidden) {
-						const note = await Notes.pack(body.id, this.user, {
+					if (this.muting.has(data.body.userId)) return;
+					if (data.body.isHidden) {
+						const note = await Notes.pack(data.body.id, this.user, {
 							detail: true
 						});
 						this.connection.cacheNote(note);
-						body = note;
+						data.body = note;
 					}
 					break;
 				}
 			}
 
-			this.send(type, body);
+			this.send(data.type, data.body);
 		});
 	}
 }
diff --git a/src/server/api/stream/channels/messaging.ts b/src/server/api/stream/channels/messaging.ts
index 015b0a7650..a75181d6d7 100644
--- a/src/server/api/stream/channels/messaging.ts
+++ b/src/server/api/stream/channels/messaging.ts
@@ -3,6 +3,8 @@ import { readUserMessagingMessage, readGroupMessagingMessage, deliverReadActivit
 import Channel from '../channel';
 import { UserGroupJoinings, Users, MessagingMessages } from '@/models/index';
 import { User, ILocalUser, IRemoteUser } from '@/models/entities/user';
+import { UserGroup } from '@/models/entities/user-group';
+import { StreamMessages } from '../types';
 
 export default class extends Channel {
 	public readonly chName = 'messaging';
@@ -12,7 +14,7 @@ export default class extends Channel {
 	private otherpartyId: string | null;
 	private otherparty: User | null;
 	private groupId: string | null;
-	private subCh: string;
+	private subCh: `messagingStream:${User['id']}-${User['id']}` | `messagingStream:${UserGroup['id']}`;
 	private typers: Record<User['id'], Date> = {};
 	private emitTypersIntervalId: ReturnType<typeof setInterval>;
 
@@ -45,7 +47,7 @@ export default class extends Channel {
 	}
 
 	@autobind
-	private onEvent(data: any) {
+	private onEvent(data: StreamMessages['messaging']['spec'] | StreamMessages['groupMessaging']['spec']) {
 		if (data.type === 'typing') {
 			const id = data.body;
 			const begin = this.typers[id] == null;
diff --git a/src/server/api/stream/channels/user-list.ts b/src/server/api/stream/channels/user-list.ts
index 0ca83cd658..63b254605b 100644
--- a/src/server/api/stream/channels/user-list.ts
+++ b/src/server/api/stream/channels/user-list.ts
@@ -3,8 +3,8 @@ import Channel from '../channel';
 import { Notes, UserListJoinings, UserLists } from '@/models/index';
 import { isMutedUserRelated } from '@/misc/is-muted-user-related';
 import { User } from '@/models/entities/user';
-import { PackedNote } from '@/models/repositories/note';
 import { isBlockerUserRelated } from '@/misc/is-blocker-user-related';
+import { Packed } from '@/misc/schema';
 
 export default class extends Channel {
 	public readonly chName = 'userList';
@@ -47,7 +47,7 @@ export default class extends Channel {
 	}
 
 	@autobind
-	private async onNote(note: PackedNote) {
+	private async onNote(note: Packed<'Note'>) {
 		if (!this.listUsers.includes(note.userId)) return;
 
 		if (['followers', 'specified'].includes(note.visibility)) {
diff --git a/src/server/api/stream/index.ts b/src/server/api/stream/index.ts
index f83bc9331e..93d56c0ac6 100644
--- a/src/server/api/stream/index.ts
+++ b/src/server/api/stream/index.ts
@@ -14,7 +14,8 @@ import { AccessToken } from '@/models/entities/access-token';
 import { UserProfile } from '@/models/entities/user-profile';
 import { publishChannelStream, publishGroupMessagingStream, publishMessagingStream } from '@/services/stream';
 import { UserGroup } from '@/models/entities/user-group';
-import { PackedNote } from '@/models/repositories/note';
+import { StreamEventEmitter, StreamMessages } from './types';
+import { Packed } from '@/misc/schema';
 
 /**
  * Main stream connection
@@ -28,10 +29,10 @@ export default class Connection {
 	public followingChannels: Set<ChannelModel['id']> = new Set();
 	public token?: AccessToken;
 	private wsConnection: websocket.connection;
-	public subscriber: EventEmitter;
+	public subscriber: StreamEventEmitter;
 	private channels: Channel[] = [];
 	private subscribingNotes: any = {};
-	private cachedNotes: PackedNote[] = [];
+	private cachedNotes: Packed<'Note'>[] = [];
 
 	constructor(
 		wsConnection: websocket.connection,
@@ -46,8 +47,8 @@ export default class Connection {
 
 		this.wsConnection.on('message', this.onWsConnectionMessage);
 
-		this.subscriber.on('broadcast', async ({ type, body }) => {
-			this.onBroadcastMessage(type, body);
+		this.subscriber.on('broadcast', data => {
+			this.onBroadcastMessage(data);
 		});
 
 		if (this.user) {
@@ -57,43 +58,41 @@ export default class Connection {
 			this.updateFollowingChannels();
 			this.updateUserProfile();
 
-			this.subscriber.on(`user:${this.user.id}`, ({ type, body }) => {
-				this.onUserEvent(type, body);
-			});
+			this.subscriber.on(`user:${this.user.id}`, this.onUserEvent);
 		}
 	}
 
 	@autobind
-	private onUserEvent(type: string, body: any) {
-		switch (type) {
+	private onUserEvent(data: StreamMessages['user']['spec']) { // { type, body }と展開すると型も展開されてしまう
+		switch (data.type) {
 			case 'follow':
-				this.following.add(body.id);
+				this.following.add(data.body.id);
 				break;
 
 			case 'unfollow':
-				this.following.delete(body.id);
+				this.following.delete(data.body.id);
 				break;
 
 			case 'mute':
-				this.muting.add(body.id);
+				this.muting.add(data.body.id);
 				break;
 
 			case 'unmute':
-				this.muting.delete(body.id);
+				this.muting.delete(data.body.id);
 				break;
 
 			// TODO: block events
 
 			case 'followChannel':
-				this.followingChannels.add(body.id);
+				this.followingChannels.add(data.body.id);
 				break;
 
 			case 'unfollowChannel':
-				this.followingChannels.delete(body.id);
+				this.followingChannels.delete(data.body.id);
 				break;
 
 			case 'updateUserProfile':
-				this.userProfile = body;
+				this.userProfile = data.body;
 				break;
 
 			case 'terminate':
@@ -145,13 +144,13 @@ export default class Connection {
 	}
 
 	@autobind
-	private onBroadcastMessage(type: string, body: any) {
-		this.sendMessageToWs(type, body);
+	private onBroadcastMessage(data: StreamMessages['broadcast']['spec']) {
+		this.sendMessageToWs(data.type, data.body);
 	}
 
 	@autobind
-	public cacheNote(note: PackedNote) {
-		const add = (note: PackedNote) => {
+	public cacheNote(note: Packed<'Note'>) {
+		const add = (note: Packed<'Note'>) => {
 			const existIndex = this.cachedNotes.findIndex(n => n.id === note.id);
 			if (existIndex > -1) {
 				this.cachedNotes[existIndex] = note;
@@ -249,7 +248,7 @@ export default class Connection {
 	}
 
 	@autobind
-	private async onNoteStreamMessage(data: any) {
+	private async onNoteStreamMessage(data: StreamMessages['note']['spec']) {
 		this.sendMessageToWs('noteUpdated', {
 			id: data.body.id,
 			type: data.type,
diff --git a/src/server/api/stream/types.ts b/src/server/api/stream/types.ts
new file mode 100644
index 0000000000..c58a627eb3
--- /dev/null
+++ b/src/server/api/stream/types.ts
@@ -0,0 +1,299 @@
+import { EventEmitter } from 'events';
+import Emitter from 'strict-event-emitter-types';
+import { Channel } from '@/models/entities/channel';
+import { User } from '@/models/entities/user';
+import { UserProfile } from '@/models/entities/user-profile';
+import { Note } from '@/models/entities/note';
+import { Antenna } from '@/models/entities/antenna';
+import { DriveFile } from '@/models/entities/drive-file';
+import { DriveFolder } from '@/models/entities/drive-folder';
+import { Emoji } from '@/models/entities/emoji';
+import { UserList } from '@/models/entities/user-list';
+import { MessagingMessage } from '@/models/entities/messaging-message';
+import { UserGroup } from '@/models/entities/user-group';
+import { ReversiGame } from '@/models/entities/games/reversi/game';
+import { AbuseUserReport } from '@/models/entities/abuse-user-report';
+import { Signin } from '@/models/entities/signin';
+import { Page } from '@/models/entities/page';
+import { Packed } from '@/misc/schema';
+
+//#region Stream type-body definitions
+export interface InternalStreamTypes {
+	antennaCreated: Antenna;
+	antennaDeleted: Antenna;
+	antennaUpdated: Antenna;
+}
+
+export interface BroadcastTypes {
+	emojiAdded: {
+		emoji: Packed<'Emoji'>;
+	};
+}
+
+export interface UserStreamTypes {
+	terminate: {};
+	followChannel: Channel;
+	unfollowChannel: Channel;
+	updateUserProfile: UserProfile;
+	mute: User;
+	unmute: User;
+	follow: Packed<'User'>;
+	unfollow: Packed<'User'>;
+	userAdded: Packed<'User'>;
+}
+
+export interface MainStreamTypes {
+	notification: Packed<'Notification'>;
+	mention: Packed<'Note'>;
+	reply: Packed<'Note'>;
+	renote: Packed<'Note'>;
+	follow: Packed<'User'>;
+	followed: Packed<'User'>;
+	unfollow: Packed<'User'>;
+	meUpdated: Packed<'User'>;
+	pageEvent: {
+		pageId: Page['id'];
+		event: string;
+		var: any;
+		userId: User['id'];
+		user: Packed<'User'>;
+	};
+	urlUploadFinished: {
+		marker?: string | null;
+		file: Packed<'DriveFile'>;
+	};
+	readAllNotifications: undefined;
+	unreadNotification: Packed<'Notification'>;
+	unreadMention: Note['id'];
+	readAllUnreadMentions: undefined;
+	unreadSpecifiedNote: Note['id'];
+	readAllUnreadSpecifiedNotes: undefined;
+	readAllMessagingMessages: undefined;
+	messagingMessage: Packed<'MessagingMessage'>;
+	unreadMessagingMessage: Packed<'MessagingMessage'>;
+	readAllAntennas: undefined;
+	unreadAntenna: Antenna;
+	readAllAnnouncements: undefined;
+	readAllChannels: undefined;
+	unreadChannel: Note['id'];
+	myTokenRegenerated: undefined;
+	reversiNoInvites: undefined;
+	reversiInvited: Packed<'ReversiMatching'>;
+	signin: Signin;
+	registryUpdated: {
+		scope?: string[];
+		key: string;
+		value: any | null;
+	};
+	driveFileCreated: Packed<'DriveFile'>;
+	readAntenna: Antenna;
+}
+
+export interface DriveStreamTypes {
+	fileCreated: Packed<'DriveFile'>;
+	fileDeleted: DriveFile['id'];
+	fileUpdated: Packed<'DriveFile'>;
+	folderCreated: Packed<'DriveFolder'>;
+	folderDeleted: DriveFolder['id'];
+	folderUpdated: Packed<'DriveFolder'>;
+}
+
+export interface NoteStreamTypes {
+	pollVoted: {
+		choice: number;
+		userId: User['id'];
+	};
+	deleted: {
+		deletedAt: Date;
+	};
+	reacted: {
+		reaction: string;
+		emoji?: Emoji;
+		userId: User['id'];
+	};
+	unreacted: {
+		reaction: string;
+		userId: User['id'];
+	};
+}
+type NoteStreamEventTypes = {
+	[key in keyof NoteStreamTypes]: {
+		id: Note['id'];
+		body: NoteStreamTypes[key];
+	};
+};
+
+export interface ChannelStreamTypes {
+	typing: User['id'];
+}
+
+export interface UserListStreamTypes {
+	userAdded: Packed<'User'>;
+	userRemoved: Packed<'User'>;
+}
+
+export interface AntennaStreamTypes {
+	note: Note;
+}
+
+export interface MessagingStreamTypes {
+	read: MessagingMessage['id'][];
+	typing: User['id'];
+	message: Packed<'MessagingMessage'>;
+	deleted: MessagingMessage['id'];
+}
+
+export interface GroupMessagingStreamTypes {
+	read: {
+		ids: MessagingMessage['id'][];
+		userId: User['id'];
+	};
+	typing: User['id'];
+	message: Packed<'MessagingMessage'>;
+	deleted: MessagingMessage['id'];
+}
+
+export interface MessagingIndexStreamTypes {
+	read: MessagingMessage['id'][];
+	message: Packed<'MessagingMessage'>;
+}
+
+export interface ReversiStreamTypes {
+	matched: Packed<'ReversiGame'>;
+	invited: Packed<'ReversiMatching'>;
+}
+
+export interface ReversiGameStreamTypes {
+	started: Packed<'ReversiGame'>;
+	ended: {
+		winnerId?: User['id'] | null,
+		game: Packed<'ReversiGame'>;
+	};
+	updateSettings: {
+		key: string;
+		value: FIXME;
+	};
+	initForm: {
+		userId: User['id'];
+		form: FIXME;
+	};
+	updateForm: {
+		userId: User['id'];
+		id: string;
+		value: FIXME;
+	};
+	message: {
+		userId: User['id'];
+		message: FIXME;
+	};
+	changeAccepts: {
+		user1: boolean;
+		user2: boolean;
+	};
+	set: {
+		at: Date;
+		color: boolean;
+		pos: number;
+		next: boolean;
+	};
+	watching: User['id'];
+}
+
+export interface AdminStreamTypes {
+	newAbuseUserReport: {
+		id: AbuseUserReport['id'];
+		targetUserId: User['id'],
+		reporterId: User['id'],
+		comment: string;
+	};
+}
+//#endregion
+
+// 辞書(interface or type)から{ type, body }ユニオンを定義
+// https://stackoverflow.com/questions/49311989/can-i-infer-the-type-of-a-value-using-extends-keyof-type
+// VS Codeの展開を防止するためにEvents型を定義
+type Events<T extends object> = { [K in keyof T]: { type: K; body: T[K]; } };
+type EventUnionFromDictionary<
+	T extends object,
+	U = Events<T>
+> = U[keyof U];
+
+// name/messages(spec) pairs dictionary
+export type StreamMessages = {
+	internal: {
+		name: 'internal';
+		spec: EventUnionFromDictionary<InternalStreamTypes>;
+	};
+	broadcast: {
+		name: 'broadcast';
+		spec: EventUnionFromDictionary<BroadcastTypes>;
+	};
+	user: {
+		name: `user:${User['id']}`;
+		spec: EventUnionFromDictionary<UserStreamTypes>;
+	};
+	main: {
+		name: `mainStream:${User['id']}`;
+		spec: EventUnionFromDictionary<MainStreamTypes>;
+	};
+	drive: {
+		name: `driveStream:${User['id']}`;
+		spec: EventUnionFromDictionary<DriveStreamTypes>;
+	};
+	note: {
+		name: `noteStream:${Note['id']}`;
+		spec: EventUnionFromDictionary<NoteStreamEventTypes>;
+	};
+	channel: {
+		name: `channelStream:${Channel['id']}`;
+		spec: EventUnionFromDictionary<ChannelStreamTypes>;
+	};
+	userList: {
+		name: `userListStream:${UserList['id']}`;
+		spec: EventUnionFromDictionary<UserListStreamTypes>;
+	};
+	antenna: {
+		name: `antennaStream:${Antenna['id']}`;
+		spec: EventUnionFromDictionary<AntennaStreamTypes>;
+	};
+	messaging: {
+		name: `messagingStream:${User['id']}-${User['id']}`;
+		spec: EventUnionFromDictionary<MessagingStreamTypes>;
+	};
+	groupMessaging: {
+		name: `messagingStream:${UserGroup['id']}`;
+		spec: EventUnionFromDictionary<GroupMessagingStreamTypes>;
+	};
+	messagingIndex: {
+		name: `messagingIndexStream:${User['id']}`;
+		spec: EventUnionFromDictionary<MessagingIndexStreamTypes>;
+	};
+	reversi: {
+		name: `reversiStream:${User['id']}`;
+		spec: EventUnionFromDictionary<ReversiStreamTypes>;
+	};
+	reversiGame: {
+		name: `reversiGameStream:${ReversiGame['id']}`;
+		spec: EventUnionFromDictionary<ReversiGameStreamTypes>;
+	};
+	admin: {
+		name: `adminStream:${User['id']}`;
+		spec: EventUnionFromDictionary<AdminStreamTypes>;
+	};
+	notes: {
+		name: 'notesStream';
+		spec: Packed<'Note'>;
+	};
+};
+
+// API event definitions
+// ストリームごとのEmitterの辞書を用意
+type EventEmitterDictionary = { [x in keyof StreamMessages]: Emitter<EventEmitter, { [y in StreamMessages[x]['name']]: (e: StreamMessages[x]['spec']) => void }> };
+// 共用体型を交差型にする型 https://stackoverflow.com/questions/54938141/typescript-convert-union-to-intersection
+type UnionToIntersection<U> = (U extends any ? (k: U) => void : never) extends ((k: infer I) => void) ? I : never;
+// Emitter辞書から共用体型を作り、UnionToIntersectionで交差型にする
+export type StreamEventEmitter = UnionToIntersection<EventEmitterDictionary[keyof StreamMessages]>;
+// { [y in name]: (e: spec) => void }をまとめてその交差型をEmitterにかけるとts(2590)にひっかかる
+
+// provide stream channels union
+export type StreamChannels = StreamMessages[keyof StreamMessages]['name'];
diff --git a/src/services/note/read.ts b/src/services/note/read.ts
index 2e221d553a..f25f86da9c 100644
--- a/src/services/note/read.ts
+++ b/src/services/note/read.ts
@@ -6,15 +6,15 @@ import { Not, IsNull, In } from 'typeorm';
 import { Channel } from '@/models/entities/channel';
 import { checkHitAntenna } from '@/misc/check-hit-antenna';
 import { getAntennas } from '@/misc/antenna-cache';
-import { PackedNote } from '@/models/repositories/note';
 import { readNotificationByQuery } from '@/server/api/common/read-notification';
+import { Packed } from '@/misc/schema';
 
 /**
  * Mark notes as read
  */
 export default async function(
 	userId: User['id'],
-	notes: (Note | PackedNote)[],
+	notes: (Note | Packed<'Note'>)[],
 	info?: {
 		following: Set<User['id']>;
 		followingChannels: Set<Channel['id']>;
@@ -34,10 +34,10 @@ export default async function(
 	})).map(x => x.followeeId));
 
 	const myAntennas = (await getAntennas()).filter(a => a.userId === userId);
-	const readMentions: (Note | PackedNote)[] = [];
-	const readSpecifiedNotes: (Note | PackedNote)[] = [];
-	const readChannelNotes: (Note | PackedNote)[] = [];
-	const readAntennaNotes: (Note | PackedNote)[] = [];
+	const readMentions: (Note | Packed<'Note'>)[] = [];
+	const readSpecifiedNotes: (Note | Packed<'Note'>)[] = [];
+	const readChannelNotes: (Note | Packed<'Note'>)[] = [];
+	const readAntennaNotes: (Note | Packed<'Note'>)[] = [];
 
 	for (const note of notes) {
 		if (note.mentions && note.mentions.includes(userId)) {
@@ -52,7 +52,7 @@ export default async function(
 
 		if (note.user != null) { // たぶんnullになることは無いはずだけど一応
 			for (const antenna of myAntennas) {
-				if (checkHitAntenna(antenna, note, note.user as any, undefined, Array.from(following))) {
+				if (await checkHitAntenna(antenna, note, note.user as any, undefined, Array.from(following))) {
 					readAntennaNotes.push(note);
 				}
 			}
diff --git a/src/services/push-notification.ts b/src/services/push-notification.ts
index dd1c32f458..de58a839df 100644
--- a/src/services/push-notification.ts
+++ b/src/services/push-notification.ts
@@ -2,13 +2,12 @@ import * as push from 'web-push';
 import config from '@/config/index';
 import { SwSubscriptions } from '@/models/index';
 import { fetchMeta } from '@/misc/fetch-meta';
-import { PackedNotification } from '../models/repositories/notification';
-import { PackedMessagingMessage } from '../models/repositories/messaging-message';
-import { pushNotificationData } from '../types';
+import { Packed } from '@/misc/schema';
+import { pushNotificationData } from '@/types';
 
 type pushNotificationsTypes = {
-	'notification': PackedNotification;
-	'unreadMessagingMessage': PackedMessagingMessage;
+	'notification': Packed<'Notification'>;
+	'unreadMessagingMessage': Packed<'MessagingMessage'>;
 	'readNotifications': { notificationIds: string[] };
 	'readAllNotifications': undefined;
 	'readAllMessagingMessages': undefined;
diff --git a/src/services/stream.ts b/src/services/stream.ts
index 4db1a77395..2c308a1b54 100644
--- a/src/services/stream.ts
+++ b/src/services/stream.ts
@@ -7,9 +7,28 @@ import { UserGroup } from '@/models/entities/user-group';
 import config from '@/config/index';
 import { Antenna } from '@/models/entities/antenna';
 import { Channel } from '@/models/entities/channel';
+import {
+	StreamChannels,
+	AdminStreamTypes,
+	AntennaStreamTypes,
+	BroadcastTypes,
+	ChannelStreamTypes,
+	DriveStreamTypes,
+	GroupMessagingStreamTypes,
+	InternalStreamTypes,
+	MainStreamTypes,
+	MessagingIndexStreamTypes,
+	MessagingStreamTypes,
+	NoteStreamTypes,
+	ReversiGameStreamTypes,
+	ReversiStreamTypes,
+	UserListStreamTypes,
+	UserStreamTypes
+} from '@/server/api/stream/types';
+import { Packed } from '@/misc/schema';
 
 class Publisher {
-	private publish = (channel: string, type: string | null, value?: any): void => {
+	private publish = (channel: StreamChannels, type: string | null, value?: any): void => {
 		const message = type == null ? value : value == null ?
 			{ type: type, body: null } :
 			{ type: type, body: value };
@@ -20,70 +39,70 @@ class Publisher {
 		}));
 	}
 
-	public publishInternalEvent = (type: string, value?: any): void => {
+	public publishInternalEvent = <K extends keyof InternalStreamTypes>(type: K, value?: InternalStreamTypes[K]): void => {
 		this.publish('internal', type, typeof value === 'undefined' ? null : value);
 	}
 
-	public publishUserEvent = (userId: User['id'], type: string, value?: any): void => {
+	public publishUserEvent = <K extends keyof UserStreamTypes>(userId: User['id'], type: K, value?: UserStreamTypes[K]): void => {
 		this.publish(`user:${userId}`, type, typeof value === 'undefined' ? null : value);
 	}
 
-	public publishBroadcastStream = (type: string, value?: any): void => {
+	public publishBroadcastStream = <K extends keyof BroadcastTypes>(type: K, value?: BroadcastTypes[K]): void => {
 		this.publish('broadcast', type, typeof value === 'undefined' ? null : value);
 	}
 
-	public publishMainStream = (userId: User['id'], type: string, value?: any): void => {
+	public publishMainStream = <K extends keyof MainStreamTypes>(userId: User['id'], type: K, value?: MainStreamTypes[K]): void => {
 		this.publish(`mainStream:${userId}`, type, typeof value === 'undefined' ? null : value);
 	}
 
-	public publishDriveStream = (userId: User['id'], type: string, value?: any): void => {
+	public publishDriveStream = <K extends keyof DriveStreamTypes>(userId: User['id'], type: K, value?: DriveStreamTypes[K]): void => {
 		this.publish(`driveStream:${userId}`, type, typeof value === 'undefined' ? null : value);
 	}
 
-	public publishNoteStream = (noteId: Note['id'], type: string, value: any): void => {
+	public publishNoteStream = <K extends keyof NoteStreamTypes>(noteId: Note['id'], type: K, value?: NoteStreamTypes[K]): void => {
 		this.publish(`noteStream:${noteId}`, type, {
 			id: noteId,
 			body: value
 		});
 	}
 
-	public publishChannelStream = (channelId: Channel['id'], type: string, value?: any): void => {
+	public publishChannelStream = <K extends keyof ChannelStreamTypes>(channelId: Channel['id'], type: K, value?: ChannelStreamTypes[K]): void => {
 		this.publish(`channelStream:${channelId}`, type, typeof value === 'undefined' ? null : value);
 	}
 
-	public publishUserListStream = (listId: UserList['id'], type: string, value?: any): void => {
+	public publishUserListStream = <K extends keyof UserListStreamTypes>(listId: UserList['id'], type: K, value?: UserListStreamTypes[K]): void => {
 		this.publish(`userListStream:${listId}`, type, typeof value === 'undefined' ? null : value);
 	}
 
-	public publishAntennaStream = (antennaId: Antenna['id'], type: string, value?: any): void => {
+	public publishAntennaStream = <K extends keyof AntennaStreamTypes>(antennaId: Antenna['id'], type: K, value?: AntennaStreamTypes[K]): void => {
 		this.publish(`antennaStream:${antennaId}`, type, typeof value === 'undefined' ? null : value);
 	}
 
-	public publishMessagingStream = (userId: User['id'], otherpartyId: User['id'], type: string, value?: any): void => {
+	public publishMessagingStream = <K extends keyof MessagingStreamTypes>(userId: User['id'], otherpartyId: User['id'], type: K, value?: MessagingStreamTypes[K]): void => {
 		this.publish(`messagingStream:${userId}-${otherpartyId}`, type, typeof value === 'undefined' ? null : value);
 	}
 
-	public publishGroupMessagingStream = (groupId: UserGroup['id'], type: string, value?: any): void => {
+	public publishGroupMessagingStream = <K extends keyof GroupMessagingStreamTypes>(groupId: UserGroup['id'], type: K, value?: GroupMessagingStreamTypes[K]): void => {
 		this.publish(`messagingStream:${groupId}`, type, typeof value === 'undefined' ? null : value);
 	}
 
-	public publishMessagingIndexStream = (userId: User['id'], type: string, value?: any): void => {
+	public publishMessagingIndexStream = <K extends keyof MessagingIndexStreamTypes>(userId: User['id'], type: K, value?: MessagingIndexStreamTypes[K]): void => {
 		this.publish(`messagingIndexStream:${userId}`, type, typeof value === 'undefined' ? null : value);
 	}
 
-	public publishReversiStream = (userId: User['id'], type: string, value?: any): void => {
+	public publishReversiStream = <K extends keyof ReversiStreamTypes>(userId: User['id'], type: K, value?: ReversiStreamTypes[K]): void => {
 		this.publish(`reversiStream:${userId}`, type, typeof value === 'undefined' ? null : value);
 	}
 
-	public publishReversiGameStream = (gameId: ReversiGame['id'], type: string, value?: any): void => {
+	public publishReversiGameStream = <K extends keyof ReversiGameStreamTypes>(gameId: ReversiGame['id'], type: K, value?: ReversiGameStreamTypes[K]): void => {
 		this.publish(`reversiGameStream:${gameId}`, type, typeof value === 'undefined' ? null : value);
 	}
 
-	public publishNotesStream = (note: any): void => {
+	public publishNotesStream = (note: Packed<'Note'>): void => {
 		this.publish('notesStream', null, note);
 	}
 
-	public publishAdminStream = (userId: User['id'], type: string, value?: any): void => {
+	public publishAdminStream = <K extends keyof AdminStreamTypes>(userId: User['id'], type: K, value?: AdminStreamTypes[K]): void => {
 		this.publish(`adminStream:${userId}`, type, typeof value === 'undefined' ? null : value);
 	}
 }
diff --git a/yarn.lock b/yarn.lock
index 08563ec3c9..5485140fc2 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -10279,6 +10279,11 @@ streamsearch@0.1.2:
   resolved "https://registry.yarnpkg.com/streamsearch/-/streamsearch-0.1.2.tgz#808b9d0e56fc273d809ba57338e929919a1a9f1a"
   integrity sha1-gIudDlb8Jz2Am6VzOOkpkZoanxo=
 
+strict-event-emitter-types@2.0.0:
+  version "2.0.0"
+  resolved "https://registry.yarnpkg.com/strict-event-emitter-types/-/strict-event-emitter-types-2.0.0.tgz#05e15549cb4da1694478a53543e4e2f4abcf277f"
+  integrity sha512-Nk/brWYpD85WlOgzw5h173aci0Teyv8YdIAEtV+N88nDB0dLlazZyJMIsN6eo1/AR61l+p6CJTG1JIyFaoNEEA==
+
 strict-uri-encode@^1.0.0:
   version "1.1.0"
   resolved "https://registry.yarnpkg.com/strict-uri-encode/-/strict-uri-encode-1.1.0.tgz#279b225df1d582b1f54e65addd4352e18faa0713"