Repository: aiscript-dev/aiscript (ts)

Issue:
docsと同じ型注釈を実際に行えるようにする
docsに書いてある型注釈が、実際のAiScriptで表記できなかったり表記法が違ったりするので、
docsを変えるか、実際にdocsのような型注釈をできるようにしたい。

## ジェネリクス
docsにはジェネリクスの型引数として`value`が導入されている。
ジェネリクスの型注釈を実装して、docsも同様の表記法に変更したい。
例:
```js
@f<T>(v: T): T { v }
let a: str = f<str>("a")
let b: num = f(12) // 呼び出し時は型引数を省略可能
```

## ユニオン型
docsに`num | null`, `str | null`, `value | null`があるが、実際にはそのような型注釈はできない。
ユニオン型の型注釈を実装したい。

## 関数型
docsには`@(name: type) { type }`のような形式になっているが、実際には`@(type) => type`の形式である。
docsの表記を実際の型注釈の表記に合わせたい。

## `fn`
docsに記載があるが、実際には型注釈として使えない。
docsの表記を具体的な型に変更したい。

## `error`, `never`
これらもdocsに記載があるが、実際には型注釈として使えない。
実際に型注釈として使えるようにしたい。


Pull request:
feat: docsと同じ型注釈を行えるようにする
<!-- ℹ お読みください
PRありがとうございます！ PRを作成する前に、以下をご確認ください:
- 可能であればタイトルに、以下で示すようなPRの種類が分かるキーワードをプリフィクスしてください。
  - fix / refactor / feat / enhance / perf / chore
  - また、PRの粒度が適切であることを確認してください。ひとつのPRに複数の種類の変更や関心を含めることは避けてください。
- このPRによって解決されるIssueがある場合は、そのIssueへの参照を本文内に含めてください。
- unreleasedディレクトリ以下にファイルを新規に作成し、変更点を記してください。リファクタリングなど、利用者に影響を与えない変更についてはこの限りではありません。
- この変更により新たに作成、もしくは更新すべきドキュメントがないか確認してください。
- 機能追加やバグ修正をした場合は、可能であればテストケースを追加してください。
- テスト、Lintが通っていることを予め確認してください。
  - `npm run test`、`npm run lint`でぞれぞれ実施可能です
- `npm run api`を実行してAPIレポートを更新し、差分がある場合はコミットしてください。
ご協力ありがとうございます🤗
-->
<!-- ℹ README
Thank you for your PR! Before creating a PR, please check the following:
- If possible, prefix the title with a keyword that identifies the type of this PR, as shown below.
  - fix / refactor / feat / enhance / perf / chore
  - Also, make sure that the granularity of this PR is appropriate. Please do not include more than one type of change or interest in a single PR.
- If there is an Issue which will be resolved by this PR, please include a reference to the Issue in the text.
- Please create a new file under the `unreleased` directory and write down the summary of your changes. However, this is not necessary for changes that do not affect the users, such as refactoring.
- Check if there are any documents that need to be created or updated due to this change.
- If you have added a feature or fixed a bug, please add a test case if possible.
- Please make sure that tests and Lint are passed in advance.
  - You can run it with `npm run test` and `npm run lint`.
- Run `npm run api` to update the API report and commit it if there are any diffs.
Thanks for your cooperation 🤗
-->

# What
<!-- このPRで何をしたのか？ どう変わるのか？ -->
<!-- What did you do with this PR? How will it change things? -->
関数宣言および関数型でのジェネリクスと、ユニオン型、error型、never型の型注釈ができるようになります。

# Why
<!-- なぜそうするのか？ どういう意図なのか？ 何が困っているのか？ -->
<!-- Why do you do it? What are your intentions? What is the problem? -->
- Resolves #856

# Additional info (optional)
<!-- テスト観点など -->
<!-- Test perspective, etc -->
common.tsから型注釈のパースを行う関数をtypes.tsに切り出しました。


Patch:
diff --git a/etc/aiscript.api.md b/etc/aiscript.api.md
index f7b63bf..ed176ac 100644
--- a/etc/aiscript.api.md
+++ b/etc/aiscript.api.md
@@ -190,7 +190,9 @@ declare namespace Ast {
         Prop,
         TypeSource,
         NamedTypeSource,
-        FnTypeSource
+        FnTypeSource,
+        UnionTypeSource,
+        TypeParam
     }
 }
 export { Ast }
@@ -320,6 +322,7 @@ const FN: (params: VUserFn["params"], statements: VUserFn["statements"], scope:
 // @public (undocumented)
 type Fn = NodeBase & {
     type: 'fn';
+    typeParams: TypeParam[];
     params: {
         dest: Expression;
         optional: boolean;
@@ -336,6 +339,7 @@ const FN_NATIVE: (fn: VNativeFn["native"]) => VNativeFn;
 // @public (undocumented)
 type FnTypeSource = NodeBase & {
     type: 'fnTypeSource';
+    typeParams: TypeParam[];
     params: TypeSource[];
     result: TypeSource;
 };
@@ -718,8 +722,19 @@ const TRUE: {
     value: boolean;
 };
 
+// @public
+type TypeParam = {
+    name: string;
+};
+
 // @public (undocumented)
-type TypeSource = NamedTypeSource | FnTypeSource;
+type TypeSource = NamedTypeSource | FnTypeSource | UnionTypeSource;
+
+// @public (undocumented)
+type UnionTypeSource = NodeBase & {
+    type: 'unionTypeSource';
+    inners: TypeSource[];
+};
 
 declare namespace utils {
     export {
diff --git a/src/interpreter/index.ts b/src/interpreter/index.ts
index 2d89264..acc12d2 100644
--- a/src/interpreter/index.ts
+++ b/src/interpreter/index.ts
@@ -833,6 +833,7 @@ export class Interpreter {
 
 			case 'namedTypeSource':
 			case 'fnTypeSource':
+			case 'unionTypeSource':
 			case 'attr': {
 				throw new Error('invalid node type');
 			}
diff --git a/src/node.ts b/src/node.ts
index 19e94e9..2c73d69 100644
--- a/src/node.ts
+++ b/src/node.ts
@@ -276,6 +276,7 @@ export type If = NodeBase & {
 
 export type Fn = NodeBase & {
 	type: 'fn'; // 関数
+	typeParams: TypeParam[]; // 型パラメータ
 	params: {
 		dest: Expression; // 引数名
 		optional: boolean;
@@ -365,7 +366,7 @@ export type Prop = NodeBase & {
 
 // Type source
 
-export type TypeSource = NamedTypeSource | FnTypeSource;
+export type TypeSource = NamedTypeSource | FnTypeSource | UnionTypeSource;
 
 export type NamedTypeSource = NodeBase & {
 	type: 'namedTypeSource'; // 名前付き型
@@ -375,6 +376,19 @@ export type NamedTypeSource = NodeBase & {
 
 export type FnTypeSource = NodeBase & {
 	type: 'fnTypeSource'; // 関数の型
+	typeParams: TypeParam[]; // 型パラメータ
 	params: TypeSource[]; // 引数の型
 	result: TypeSource; // 戻り値の型
 };
+
+export type UnionTypeSource = NodeBase & {
+	type: 'unionTypeSource'; // ユニオン型
+	inners: TypeSource[]; // 含まれる型
+};
+
+/**
+ * 型パラメータ
+ */
+export type TypeParam = {
+	name: string; // パラメータ名
+}
diff --git a/src/parser/plugins/validate-keyword.ts b/src/parser/plugins/validate-keyword.ts
index 024a1de..9ccfad7 100644
--- a/src/parser/plugins/validate-keyword.ts
+++ b/src/parser/plugins/validate-keyword.ts
@@ -79,6 +79,14 @@ function validateDest(node: Ast.Node): Ast.Node {
 	});
 }
 
+function validateTypeParams(node: Ast.Fn | Ast.FnTypeSource): void {
+	for (const typeParam of node.typeParams) {
+		if (reservedWord.includes(typeParam.name)) {
+			throwReservedWordError(typeParam.name, node.loc);
+		}
+	}
+}
+
 function validateNode(node: Ast.Node): Ast.Node {
 	switch (node.type) {
 		case 'def': {
@@ -111,6 +119,7 @@ function validateNode(node: Ast.Node): Ast.Node {
 			break;
 		}
 		case 'fn': {
+			validateTypeParams(node);
 			for (const param of node.params) {
 				validateDest(param.dest);
 			}
@@ -124,6 +133,16 @@ function validateNode(node: Ast.Node): Ast.Node {
 			}
 			break;
 		}
+		case 'namedTypeSource': {
+			if (reservedWord.includes(node.name)) {
+				throwReservedWordError(node.name, node.loc);
+			}
+			break;
+		}
+		case 'fnTypeSource': {
+			validateTypeParams(node);
+			break;
+		}
 	}
 
 	return node;
diff --git a/src/parser/plugins/validate-type.ts b/src/parser/plugins/validate-type.ts
index 6fc8660..1d04391 100644
--- a/src/parser/plugins/validate-type.ts
+++ b/src/parser/plugins/validate-type.ts
@@ -2,22 +2,43 @@ import { getTypeBySource } from '../../type.js';
 import { visitNode } from '../visit.js';
 import type * as Ast from '../../node.js';
 
-function validateNode(node: Ast.Node): Ast.Node {
+function collectTypeParams(node: Ast.Node, ancestors: Ast.Node[]): Ast.TypeParam[] {
+	const items = [];
+	if (node.type === 'fn') {
+		const typeParamNames = new Set<string>();
+		for (const typeParam of node.typeParams) {
+			if (typeParamNames.has(typeParam.name)) {
+				throw new Error(`type parameter name ${typeParam.name} is duplicate`);
+			}
+			typeParamNames.add(typeParam.name);
+		}
+		items.push(...node.typeParams);
+	}
+	for (let i = ancestors.length - 1; i >= 0; i--) {
+		const ancestor = ancestors[i]!;
+		if (ancestor.type === 'fn') {
+			items.push(...ancestor.typeParams);
+		}
+	}
+	return items;
+}
+
+function validateNode(node: Ast.Node, ancestors: Ast.Node[]): Ast.Node {
 	switch (node.type) {
 		case 'def': {
 			if (node.varType != null) {
-				getTypeBySource(node.varType);
+				getTypeBySource(node.varType, collectTypeParams(node, ancestors));
 			}
 			break;
 		}
 		case 'fn': {
 			for (const param of node.params) {
 				if (param.argType != null) {
-					getTypeBySource(param.argType);
+					getTypeBySource(param.argType, collectTypeParams(node, ancestors));
 				}
 			}
 			if (node.retType != null) {
-				getTypeBySource(node.retType);
+				getTypeBySource(node.retType, collectTypeParams(node, ancestors));
 			}
 			break;
 		}
diff --git a/src/parser/scanner.ts b/src/parser/scanner.ts
index 401f2bd..5cab470 100644
--- a/src/parser/scanner.ts
+++ b/src/parser/scanner.ts
@@ -304,7 +304,7 @@ export class Scanner implements ITokenStream {
 						this.stream.next();
 						return TOKEN(TokenKind.Or2, pos, { hasLeftSpacing });
 					} else {
-						throw new AiScriptSyntaxError('invalid character: "|"', pos);
+						return TOKEN(TokenKind.Or, pos, { hasLeftSpacing });
 					}
 				}
 				case '}': {
diff --git a/src/parser/syntaxes/common.ts b/src/parser/syntaxes/common.ts
index a921682..946c112 100644
--- a/src/parser/syntaxes/common.ts
+++ b/src/parser/syntaxes/common.ts
@@ -3,6 +3,7 @@ import { AiScriptSyntaxError, AiScriptUnexpectedEOFError } from '../../error.js'
 import { NODE } from '../utils.js';
 import { parseStatement } from './statements.js';
 import { parseExpr } from './expressions.js';
+import { parseType } from './types.js';
 
 import type { ITokenStream } from '../streams/token-stream.js';
 import type * as Ast from '../../node.js';
@@ -134,16 +135,6 @@ export function parseBlock(s: ITokenStream): (Ast.Statement | Ast.Expression)[]
 	return steps;
 }
 
-//#region Type
-
-export function parseType(s: ITokenStream): Ast.TypeSource {
-	if (s.is(TokenKind.At)) {
-		return parseFnType(s);
-	} else {
-		return parseNamedType(s);
-	}
-}
-
 /**
  * ```abnf
  * OptionalSeparator = [SEP]
@@ -167,79 +158,3 @@ export function parseOptionalSeparator(s: ITokenStream): boolean {
 		}
 	}
 }
-
-/**
- * ```abnf
- * FnType = "@" "(" ParamTypes ")" "=>" Type
- * ParamTypes = [Type *(SEP Type)]
- * ```
-*/
-function parseFnType(s: ITokenStream): Ast.TypeSource {
-	const startPos = s.getPos();
-
-	s.expect(TokenKind.At);
-	s.next();
-	s.expect(TokenKind.OpenParen);
-	s.next();
-
-	const params: Ast.TypeSource[] = [];
-	while (!s.is(TokenKind.CloseParen)) {
-		if (params.length > 0) {
-			switch (s.getTokenKind()) {
-				case TokenKind.Comma: {
-					s.next();
-					break;
-				}
-				case TokenKind.EOF: {
-					throw new AiScriptUnexpectedEOFError(s.getPos());
-				}
-				default: {
-					throw new AiScriptSyntaxError('separator expected', s.getPos());
-				}
-			}
-		}
-		const type = parseType(s);
-		params.push(type);
-	}
-
-	s.expect(TokenKind.CloseParen);
-	s.next();
-	s.expect(TokenKind.Arrow);
-	s.next();
-
-	const resultType = parseType(s);
-
-	return NODE('fnTypeSource', { params, result: resultType }, startPos, s.getPos());
-}
-
-/**
- * ```abnf
- * NamedType = IDENT ["<" Type ">"]
- * ```
-*/
-function parseNamedType(s: ITokenStream): Ast.TypeSource {
-	const startPos = s.getPos();
-
-	let name: string;
-	if (s.is(TokenKind.Identifier)) {
-		name = s.getTokenValue();
-		s.next();
-	} else {
-		s.expect(TokenKind.NullKeyword);
-		s.next();
-		name = "null";
-	}
-
-	// inner type
-	let inner: Ast.TypeSource | undefined;
-	if (s.is(TokenKind.Lt)) {
-		s.next();
-		inner = parseType(s);
-		s.expect(TokenKind.Gt);
-		s.next();
-	}
-
-	return NODE('namedTypeSource', { name, inner }, startPos, s.getPos());
-}
-
-//#endregion Type
diff --git a/src/parser/syntaxes/expressions.ts b/src/parser/syntaxes/expressions.ts
index 8ef6b35..5d6e264 100644
--- a/src/parser/syntaxes/expressions.ts
+++ b/src/parser/syntaxes/expressions.ts
@@ -2,8 +2,9 @@ import { AiScriptSyntaxError, AiScriptUnexpectedEOFError } from '../../error.js'
 import { NODE, unexpectedTokenError } from '../utils.js';
 import { TokenStream } from '../streams/token-stream.js';
 import { TokenKind } from '../token.js';
-import { parseBlock, parseOptionalSeparator, parseParams, parseType } from './common.js';
+import { parseBlock, parseOptionalSeparator, parseParams } from './common.js';
 import { parseBlockOrStatement } from './statements.js';
+import { parseType, parseTypeParams } from './types.js';
 
 import type * as Ast from '../../node.js';
 import type { ITokenStream } from '../streams/token-stream.js';
@@ -381,7 +382,7 @@ function parseIf(s: ITokenStream): Ast.If {
 
 /**
  * ```abnf
- * FnExpr = "@" Params [":" Type] Block
+ * FnExpr = "@" [TypeParams] Params [":" Type] Block
  * ```
 */
 function parseFnExpr(s: ITokenStream): Ast.Fn {
@@ -390,6 +391,13 @@ function parseFnExpr(s: ITokenStream): Ast.Fn {
 	s.expect(TokenKind.At);
 	s.next();
 
+	let typeParams: Ast.TypeParam[];
+	if (s.is(TokenKind.Lt)) {
+		typeParams = parseTypeParams(s);
+	} else {
+		typeParams = [];
+	}
+
 	const params = parseParams(s);
 
 	let type: Ast.TypeSource | undefined;
@@ -400,7 +408,7 @@ function parseFnExpr(s: ITokenStream): Ast.Fn {
 
 	const body = parseBlock(s);
 
-	return NODE('fn', { params: params, retType: type, children: body }, startPos, s.getPos());
+	return NODE('fn', { typeParams, params, retType: type, children: body }, startPos, s.getPos());
 }
 
 /**
diff --git a/src/parser/syntaxes/statements.ts b/src/parser/syntaxes/statements.ts
index 5097313..050c15d 100644
--- a/src/parser/syntaxes/statements.ts
+++ b/src/parser/syntaxes/statements.ts
@@ -1,8 +1,9 @@
 import { AiScriptSyntaxError } from '../../error.js';
 import { CALL_NODE, NODE, unexpectedTokenError } from '../utils.js';
 import { TokenKind } from '../token.js';
-import { parseBlock, parseDest, parseParams, parseType } from './common.js';
+import { parseBlock, parseDest, parseParams } from './common.js';
 import { parseExpr } from './expressions.js';
+import { parseType, parseTypeParams } from './types.js';
 
 import type * as Ast from '../../node.js';
 import type { ITokenStream } from '../streams/token-stream.js';
@@ -144,7 +145,7 @@ function parseVarDef(s: ITokenStream): Ast.Definition {
 
 /**
  * ```abnf
- * FnDef = "@" IDENT Params [":" Type] Block
+ * FnDef = "@" IDENT [TypeParams] Params [":" Type] Block
  * ```
 */
 function parseFnDef(s: ITokenStream): Ast.Definition {
@@ -159,6 +160,13 @@ function parseFnDef(s: ITokenStream): Ast.Definition {
 	s.next();
 	const dest = NODE('identifier', { name }, nameStartPos, s.getPos());
 
+	let typeParams: Ast.TypeParam[];
+	if (s.is(TokenKind.Lt)) {
+		typeParams = parseTypeParams(s);
+	} else {
+		typeParams = [];
+	}
+
 	const params = parseParams(s);
 
 	let type: Ast.TypeSource | undefined;
@@ -174,6 +182,7 @@ function parseFnDef(s: ITokenStream): Ast.Definition {
 	return NODE('def', {
 		dest,
 		expr: NODE('fn', {
+			typeParams,
 			params: params,
 			retType: type,
 			children: body,
diff --git a/src/parser/syntaxes/types.ts b/src/parser/syntaxes/types.ts
new file mode 100644
index 0000000..a29ced1
--- /dev/null
+++ b/src/parser/syntaxes/types.ts
@@ -0,0 +1,176 @@
+import { AiScriptSyntaxError, AiScriptUnexpectedEOFError } from '../../error.js';
+import { TokenKind } from '../token.js';
+import { NODE } from '../utils.js';
+import { parseOptionalSeparator } from './common.js';
+
+import type { Ast } from '../../index.js';
+import type { ITokenStream } from '../streams/token-stream.js';
+import type { TypeParam } from '../../node.js';
+
+/**
+ * ```abnf
+ * Type = FnType / NamedType
+ * ```
+*/
+export function parseType(s: ITokenStream): Ast.TypeSource {
+	return parseUnionType(s);
+}
+
+/**
+ * ```abnf
+ * TypeParams = "<" TypeParam *(SEP TypeParam) [SEP] ">"
+ * ```
+*/
+export function parseTypeParams(s: ITokenStream): TypeParam[] {
+	s.expect(TokenKind.Lt);
+	s.next();
+
+	if (s.is(TokenKind.NewLine)) {
+		s.next();
+	}
+
+	const items: TypeParam[] = [parseTypeParam(s)];
+
+	while (parseOptionalSeparator(s)) {
+		if (s.is(TokenKind.Gt)) {
+			break;
+		}
+		const item = parseTypeParam(s);
+		items.push(item);
+	}
+
+	s.expect(TokenKind.Gt);
+	s.next();
+
+	return items;
+}
+
+/**
+ * ```abnf
+ * TypeParam = IDENT
+ * ```
+*/
+function parseTypeParam(s: ITokenStream): TypeParam {
+	s.expect(TokenKind.Identifier);
+	const name = s.getTokenValue();
+	s.next();
+
+	return { name };
+}
+
+/**
+ * ```abnf
+ * UnionType = UnionTypeInner *("|" UnionTypeInner)
+ * ```
+*/
+function parseUnionType(s: ITokenStream): Ast.TypeSource {
+	const startPos = s.getPos();
+
+	const first = parseUnionTypeInner(s);
+	if (!s.is(TokenKind.Or)) {
+		return first;
+	}
+
+	const inners = [first];
+	do {
+		s.next();
+		inners.push(parseUnionTypeInner(s));
+	} while (s.is(TokenKind.Or));
+
+	return NODE('unionTypeSource', { inners }, startPos, s.getPos());
+}
+
+/**
+ * ```abnf
+ * UnionTypeTerm = FnType / NamedType
+ * ```
+*/
+function parseUnionTypeInner(s: ITokenStream): Ast.TypeSource {
+	if (s.is(TokenKind.At)) {
+		return parseFnType(s);
+	} else {
+		return parseNamedType(s);
+	}
+}
+
+/**
+ * ```abnf
+ * FnType = "@" [TypeParams] "(" ParamTypes ")" "=>" Type
+ * ParamTypes = [Type *(SEP Type)]
+ * ```
+*/
+function parseFnType(s: ITokenStream): Ast.TypeSource {
+	const startPos = s.getPos();
+
+	s.expect(TokenKind.At);
+	s.next();
+
+	let typeParams: Ast.TypeParam[];
+	if (s.is(TokenKind.Lt)) {
+		typeParams = parseTypeParams(s);
+	} else {
+		typeParams = [];
+	}
+
+	s.expect(TokenKind.OpenParen);
+	s.next();
+
+	const params: Ast.TypeSource[] = [];
+	while (!s.is(TokenKind.CloseParen)) {
+		if (params.length > 0) {
+			switch (s.getTokenKind()) {
+				case TokenKind.Comma: {
+					s.next();
+					break;
+				}
+				case TokenKind.EOF: {
+					throw new AiScriptUnexpectedEOFError(s.getPos());
+				}
+				default: {
+					throw new AiScriptSyntaxError('separator expected', s.getPos());
+				}
+			}
+		}
+		const type = parseType(s);
+		params.push(type);
+	}
+
+	s.expect(TokenKind.CloseParen);
+	s.next();
+	s.expect(TokenKind.Arrow);
+	s.next();
+
+	const resultType = parseType(s);
+
+	return NODE('fnTypeSource', { typeParams, params, result: resultType }, startPos, s.getPos());
+}
+
+/**
+ * ```abnf
+ * NamedType = IDENT ["<" Type ">"]
+ * ```
+*/
+function parseNamedType(s: ITokenStream): Ast.TypeSource {
+	const startPos = s.getPos();
+
+	let name: string;
+	if (s.is(TokenKind.Identifier)) {
+		name = s.getTokenValue();
+		s.next();
+	} else {
+		s.expect(TokenKind.NullKeyword);
+		s.next();
+		name = 'null';
+	}
+
+	// inner type
+	let inner: Ast.TypeSource | undefined;
+	if (s.is(TokenKind.Lt)) {
+		s.next();
+		inner = parseType(s);
+		s.expect(TokenKind.Gt);
+		s.next();
+	}
+
+	return NODE('namedTypeSource', { name, inner }, startPos, s.getPos());
+}
diff --git a/src/parser/token.ts b/src/parser/token.ts
index d4bdaf4..94425cd 100644
--- a/src/parser/token.ts
+++ b/src/parser/token.ts
@@ -103,6 +103,8 @@ export enum TokenKind {
 	Hat,
 	/** "{" */
 	OpenBrace,
+	/** "|" */
+	Or,
 	/** "||" */
 	Or2,
 	/** "}" */
diff --git a/src/parser/visit.ts b/src/parser/visit.ts
index efe878b..a568699 100644
--- a/src/parser/visit.ts
+++ b/src/parser/visit.ts
@@ -11,6 +11,9 @@ function visitNodeInner(node: Ast.Node, fn: (node: Ast.Node, ancestors: Ast.Node
 	// nested nodes
 	switch (result.type) {
 		case 'def': {
+			if (result.varType != null) {
+				result.varType = visitNodeInner(result.varType, fn, ancestors) as Ast.Definition['varType'];
+			}
 			result.attr = result.attr.map((attr) => visitNodeInner(attr, fn, ancestors) as Ast.Attribute);
 			result.expr = visitNodeInner(result.expr, fn, ancestors) as Ast.Definition['expr'];
 			break;
@@ -79,6 +82,12 @@ function visitNodeInner(node: Ast.Node, fn: (node: Ast.Node, ancestors: Ast.Node
 				if (param.default) {
 					param.default = visitNodeInner(param.default!, fn, ancestors) as Ast.Fn['params'][number]['default'];
 				}
+				if (param.argType != null) {
+					param.argType = visitNodeInner(param.argType, fn, ancestors) as Ast.Fn['params'][number]['argType'];
+				}
+			}
+			if (result.retType != null) {
+				result.retType = visitNodeInner(result.retType, fn, ancestors) as Ast.Fn['retType'];
 			}
 			for (let i = 0; i < result.children.length; i++) {
 				result.children[i] = visitNodeInner(result.children[i]!, fn, ancestors) as Ast.Fn['children'][number];
@@ -198,6 +207,19 @@ function visitNodeInner(node: Ast.Node, fn: (node: Ast.Node, ancestors: Ast.Node
 			)['right'];
 			break;
 		}
+
+		case 'fnTypeSource': {
+			for (let i = 0; i < result.params.length; i++) {
+				result.params[i] = visitNodeInner(result.params[i]!, fn, ancestors) as Ast.FnTypeSource['params'][number];
+			}
+			break;
+		}
+		case 'unionTypeSource': {
+			for (let i = 0; i < result.inners.length; i++) {
+				result.inners[i] = visitNodeInner(result.inners[i]!, fn, ancestors) as Ast.UnionTypeSource['inners'][number];
+			}
+			break;
+		}
 	}
 
 	ancestors.pop();
diff --git a/src/type.ts b/src/type.ts
index 92f03b4..4f2d6e6 100644
--- a/src/type.ts
+++ b/src/type.ts
@@ -47,7 +47,31 @@ export function T_FN(params: Type[], result: Type): TFn {
 	};
 }
 
-export type Type = TSimple | TGeneric | TFn;
+export type TParam = {
+	type: 'param';
+	name: string;
+}
+
+export function T_PARAM(name: string): TParam {
+	return {
+		type: 'param',
+		name,
+	};
+}
+
+export type TUnion = {
+	type: 'union';
+	inners: Type[];
+}
+
+export function T_UNION(inners: Type[]): TUnion {
+	return {
+		type: 'union',
+		inners,
+	};
+}
+
+export type Type = TSimple | TGeneric | TFn | TParam | TUnion;
 
 function assertTSimple(t: Type): asserts t is TSimple { if (t.type !== 'simple') { throw new TypeError('assertTSimple failed.'); } }
 function assertTGeneric(t: Type): asserts t is TGeneric { if (t.type !== 'generic') { throw new TypeError('assertTGeneric failed.'); } }
@@ -87,6 +111,14 @@ export function isCompatibleType(a: Type, b: Type): boolean {
 			}
 			break;
 		}
+		case 'param': {
+			// TODO
+			break;
+		}
+		case 'union': {
+			// TODO
+			break;
+		}
 	}
 
 	return true;
@@ -103,6 +135,12 @@ export function getTypeName(type: Type): string {
 		case 'fn': {
 			return `@(${type.params.map(param => getTypeName(param)).join(', ')}) { ${getTypeName(type.result)} }`;
 		}
+		case 'param': {
+			return type.name;
+		}
+		case 'union': {
+			return type.inners.join(' | ');
+		}
 	}
 }
 
@@ -121,17 +159,27 @@ export function getTypeNameBySource(typeSource: Ast.TypeSource): string {
 			const result = getTypeNameBySource(typeSource.result);
 			return `@(${params}) { ${result} }`;
 		}
+		case 'unionTypeSource': {
+			return typeSource.inners.map(inner => getTypeBySource(inner)).join(' | ');
+		}
 	}
 }
 
-export function getTypeBySource(typeSource: Ast.TypeSource): Type {
+export function getTypeBySource(typeSource: Ast.TypeSource, typeParams?: readonly Ast.TypeParam[]): Type {
 	if (typeSource.type === 'namedTypeSource') {
+		const typeParam = typeParams?.find((param) => param.name === typeSource.name);
+		if (typeParam != null) {
+			return T_PARAM(typeParam.name);
+		}
+
 		switch (typeSource.name) {
 			// simple types
 			case 'null':
 			case 'bool':
 			case 'num':
 			case 'str':
+			case 'error':
+			case 'never':
 			case 'any':
 			case 'void': {
 				if (typeSource.inner == null) {
@@ -144,7 +192,7 @@ export function getTypeBySource(typeSource: Ast.TypeSource): Type {
 			case 'obj': {
 				let innerType: Type;
 				if (typeSource.inner != null) {
-					innerType = getTypeBySource(typeSource.inner);
+					innerType = getTypeBySource(typeSource.inner, typeParams);
 				} else {
 					innerType = T_SIMPLE('any');
 				}
@@ -152,8 +200,15 @@ export function getTypeBySource(typeSource: Ast.TypeSource): Type {
 			}
 		}
 		throw new AiScriptSyntaxError(`Unknown type: '${getTypeNameBySource(typeSource)}'`, typeSource.loc.start);
+	} else if (typeSource.type === 'fnTypeSource') {
+		let fnTypeParams = typeSource.typeParams;
+		if (typeParams != null) {
+			fnTypeParams = fnTypeParams.concat(typeParams);
+		}
+		const paramTypes = typeSource.params.map(param => getTypeBySource(param, fnTypeParams));
+		return T_FN(paramTypes, getTypeBySource(typeSource.result, fnTypeParams));
 	} else {
-		const paramTypes = typeSource.params.map(param => getTypeBySource(param));
-		return T_FN(paramTypes, getTypeBySource(typeSource.result));
+		const innerTypes = typeSource.inners.map(inner => getTypeBySource(inner, typeParams));
+		return T_UNION(innerTypes);
 	}
 }
diff --git a/unreleased/unified-type-annotation.md b/unreleased/unified-type-annotation.md
new file mode 100644
index 0000000..480891d
--- /dev/null
+++ b/unreleased/unified-type-annotation.md
@@ -0,0 +1,5 @@
+- 以下の型注釈ができるようになりました。
+  - 関数宣言および関数型でのジェネリクス
+  - ユニオン型
+  - error型
+  - never型


Test patch:
diff --git a/test/keywords.ts b/test/keywords.ts
index ca9fca1..c318b3c 100644
--- a/test/keywords.ts
+++ b/test/keywords.ts
@@ -110,6 +110,11 @@ const sampleCodes = Object.entries<(word: string) => string>({
 	`
 	### ${word} 1
 	`,
+
+	typeParam: word =>
+	`
+	@f<${word}>(x): ${word} { x }
+	`,
 });
 
 const parser = new Parser();
diff --git a/test/newline.ts b/test/newline.ts
index 8d19501..3425d7b 100644
--- a/test/newline.ts
+++ b/test/newline.ts
@@ -95,6 +95,64 @@ describe('empty lines', () => {
 		});
     });
 
+    describe('type params', () => {
+        describe('function', () => {
+            test.concurrent('empty line before', async () => {
+                const res = await exe(`
+                @f<
+                    // comment
+                    T
+                >(v: T): T {
+                    v
+                }
+                <: f(1)
+                `);
+                eq(res, NUM(1));
+            });
+
+            test.concurrent('empty line after', async () => {
+                const res = await exe(`
+                @f<
+                    T
+                    // comment
+                >(v: T): T {
+                    v
+                }
+                <: f(1)
+                `);
+                eq(res, NUM(1));
+            });
+        });
+
+        describe('function type', () => {
+            test.concurrent('empty line before', async () => {
+                const res = await exe(`
+                let f: @<
+                    // comment
+                    T
+                >(T) => T = @(v) {
+                    v
+                }
+                <: f(1)
+                `);
+                eq(res, NUM(1));
+            });
+
+            test.concurrent('empty line after', async () => {
+                const res = await exe(`
+                let f: @<
+                    T
+                    // comment
+                >(T) => T = @(v) {
+                    v
+                }
+                <: f(1)
+                `);
+                eq(res, NUM(1));
+            });
+        });
+    });
+
     describe('function params', () => {
 		test.concurrent('empty line', async () => {
 			const res = await exe(`
diff --git a/test/types.ts b/test/types.ts
new file mode 100644
index 0000000..d876e00
--- /dev/null
+++ b/test/types.ts
@@ -0,0 +1,200 @@
+import * as assert from 'assert';
+import { describe, test } from 'vitest';
+import { utils } from '../src';
+import { NUM, STR, NULL, ARR, OBJ, BOOL, TRUE, FALSE, ERROR ,FN_NATIVE } from '../src/interpreter/value';
+import { AiScriptRuntimeError } from '../src/error';
+import { exe, getMeta, eq } from './testutils';
+
+describe('function types', () => {
+	test.concurrent('multiple params', async () => {
+		const res = await exe(`
+		let f: @(str, num) => bool = @() { true }
+		<: f('abc', 123)
+		`);
+		eq(res, TRUE);
+	});
+});
+
+describe('generics', () => {
+	describe('function', () => {
+		test.concurrent('expr', async () => {
+			const res = await exe(`
+				let f = @<T>(v: T): void {}
+				<: f("a")
+			`);
+			eq(res, NULL);
+		});
+
+		test.concurrent('consumer', async () => {
+			const res = await exe(`
+			@f<T>(v: T): void {}
+			<: f("a")
+			`);
+			eq(res, NULL);
+		});
+
+		test.concurrent('identity function', async () => {
+			const res = await exe(`
+			@f<T>(v: T): T { v }
+			<: f(1)
+			`);
+			eq(res, NUM(1));
+		});
+
+		test.concurrent('use as inner type', async () => {
+			const res = await exe(`
+			@vals<T>(v: obj<T>): arr<T> {
+				Obj:vals(v)
+			}
+			<: vals({ a: 1, b: 2, c: 3 })
+			`);
+			eq(res, ARR([NUM(1), NUM(2), NUM(3)]));
+		});
+
+		test.concurrent('use as variable type', async () => {
+			const res = await exe(`
+			@f<T>(v: T): void {
+				let v2: T = v
+			}
+			<: f(1)
+			`);
+			eq(res, NULL);
+		});
+
+		test.concurrent('use as function type', async () => {
+			const res = await exe(`
+			@f<T>(v: T): @() => T {
+				let g: @() => T = @() { v }
+				g
+			}
+			<: f(1)()
+			`);
+			eq(res, NUM(1))
+		});
+
+		test.concurrent('curried', async () => {
+			const res = await exe(`
+			@concat<A>(a: A): @<B>(B) => str {
+				@<B>(b: B) {
+					\`{a}{b}\`
+				}
+			}
+			<: concat("abc")(123)
+			`);
+			eq(res, STR('abc123'));
+		});
+
+		test.concurrent('new lines', async () => {
+			const res = await exe(`
+			@f<
+				T
+				U
+			>(x: T, y: U): arr<T | U> {
+				[x, y]
+			}
+			<: f("abc", 123)
+			`);
+			eq(res, ARR([STR('abc'), NUM(123)]));
+		});
+
+		test.concurrent('duplicate', async () => {
+			await assert.rejects(() => exe(`
+			@f<T, T>(v: T) {}
+			`));
+		});
+
+		test.concurrent('empty', async () => {
+			await assert.rejects(() => exe(`
+			@f<>() {}
+			`));
+		});
+	});
+});
+
+describe('union', () => {
+	test.concurrent('variable type', async () => {
+		const res = await exe(`
+		let a: num | null = null
+		<: a
+		`);
+		eq(res, NULL);
+	});
+
+	test.concurrent('more inners', async () => {
+		const res = await exe(`
+		let a: str | num | null = null
+		<: a
+		`);
+		eq(res, NULL);
+	});
+
+	test.concurrent('inner type', async () => {
+		const res = await exe(`
+		let a: arr<num | str> = ["abc", 123]
+		<: a
+		`);
+		eq(res, ARR([STR('abc'), NUM(123)]));
+	});
+
+	test.concurrent('param type', async () => {
+		const res = await exe(`
+		@f(x: num | str): str {
+			\`{x}\`
+		}
+		<: f(1)
+		`);
+		eq(res, STR('1'));
+	});
+
+	test.concurrent('return type', async () => {
+		const res = await exe(`
+		@f(): num | str { 1 }
+		<: f()
+		`);
+		eq(res, NUM(1));
+	});
+
+    test.concurrent('type parameter', async () => {
+        const res = await exe(`
+        @f<T>(v: T): T | null { null }
+        <: f(1)
+        `);
+        eq(res, NULL);
+    });
+
+	test.concurrent('function type', async () => {
+		const res = await exe(`
+		let f: @(num | str) => str = @(x) { \`{x}\` }
+		<: f(1)
+		`);
+		eq(res, STR('1'));
+	});
+
+	test.concurrent('invalid inner', async () => {
+		await assert.rejects(() => exe(`
+		let a: ThisIsAnInvalidTypeName | null = null
+		`));
+	});
+});
+
+describe('simple', () => {
+	test.concurrent('error', async () => {
+		const res = await exe(`
+		let a: error = Error:create("Ai")
+		<: a
+		`);
+		eq(res, ERROR('Ai'));
+	});
+
+	test.concurrent('never', async () => {
+		const res = await exe(`
+		@f() {
+			let a: never = eval {
+				return 1
+			}
+		}
+		<: f()
+		`);
+		eq(res, NUM(1));
+	});
+});