Repository: aiscript-dev/aiscript
Pull request #879 (2024-12-17 13:23:05)

Bump nanoid from 3.3.7 to 3.3.8 in /playground
Bumps [nanoid](https://github.com/ai/nanoid) from 3.3.7 to 3.3.8.
<details>
<summary>Changelog</summary>
<p><em>Sourced from <a href="https://github.com/ai/nanoid/blob/main/CHANGELOG.md">nanoid's changelog</a>.</em></p>
<blockquote>
<h2>3.3.8</h2>
<ul>
<li>Fixed a way to break Nano ID by passing non-integer size (by <a href="https://github.com/myndzi"><code>@​myndzi</code></a>).</li>
</ul>
</blockquote>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a href="https://github.com/ai/nanoid/commit/3044cd5e73f4cf31795f61f6e6b961c8c0a5c744"><code>3044cd5</code></a> Release 3.3.8 version</li>
<li><a href="https://github.com/ai/nanoid/commit/4fe34959c34e5b3573889ed4f24fe91d1d3e7231"><code>4fe3495</code></a> Update size limit</li>
<li><a href="https://github.com/ai/nanoid/commit/d643045f40d6dc8afa000a644d857da1436ed08c"><code>d643045</code></a> Fix pool pollution, infinite loop (<a href="https://redirect.github.com/ai/nanoid/issues/510">#510</a>)</li>
<li>See full diff in <a href="https://github.com/ai/nanoid/compare/3.3.7...3.3.8">compare view</a></li>
</ul>
</details>
<br />


[![Dependabot compatibility score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=nanoid&package-manager=npm_and_yarn&previous-version=3.3.7&new-version=3.3.8)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores)

Dependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually by commenting `@dependabot rebase`.

[//]: # (dependabot-automerge-start)
[//]: # (dependabot-automerge-end)

---

<details>
<summary>Dependabot commands and options</summary>
<br />

You can trigger Dependabot actions by commenting on this PR:
- `@dependabot rebase` will rebase this PR
- `@dependabot recreate` will recreate this PR, overwriting any edits that have been made to it
- `@dependabot merge` will merge this PR after your CI passes on it
- `@dependabot squash and merge` will squash and merge this PR after your CI passes on it
- `@dependabot cancel merge` will cancel a previously requested merge and block automerging
- `@dependabot reopen` will reopen this PR if it is closed
- `@dependabot close` will close this PR and stop Dependabot recreating it. You can achieve the same result by closing it manually
- `@dependabot show <dependency name> ignore conditions` will show all of the ignore conditions of the specified dependency
- `@dependabot ignore this major version` will close this PR and stop Dependabot creating any more for this major version (unless you reopen the PR or upgrade to it yourself)
- `@dependabot ignore this minor version` will close this PR and stop Dependabot creating any more for this minor version (unless you reopen the PR or upgrade to it yourself)
- `@dependabot ignore this dependency` will close this PR and stop Dependabot creating any more for this dependency (unless you reopen the PR or upgrade to it yourself)
You can disable automated security fix PRs for this repo from the [Security Alerts page](https://github.com/aiscript-dev/aiscript/network/alerts).

</details>


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/playground/package-lock.json b/playground/package-lock.json
index 806ce54..0d7c797 100644
--- a/playground/package-lock.json
+++ b/playground/package-lock.json
@@ -26,24 +26,24 @@
       "dependencies": {
         "seedrandom": "3.0.5",
         "stringz": "2.1.0",
-        "uuid": "10.0.0"
+        "uuid": "11.0.3"
       },
       "devDependencies": {
-        "@eslint/js": "9.13.0",
-        "@microsoft/api-extractor": "7.47.11",
+        "@eslint/js": "9.15.0",
+        "@microsoft/api-extractor": "7.48.0",
         "@types/eslint__js": "8.42.3",
-        "@types/node": "22.7.9",
+        "@types/node": "22.10.1",
         "@types/seedrandom": "3.0.8",
         "@types/uuid": "10.0.0",
-        "@vitest/coverage-v8": "2.1.3",
+        "@vitest/coverage-v8": "2.1.6",
         "chalk": "5.3.0",
-        "eslint": "9.13.0",
+        "eslint": "9.15.0",
         "eslint-plugin-import": "2.31.0",
         "semver": "7.6.3",
         "ts-node": "10.9.2",
-        "typescript": "5.6.3",
-        "typescript-eslint": "8.11.0",
-        "vitest": "2.1.3"
+        "typescript": "5.7.2",
+        "typescript-eslint": "8.16.0",
+        "vitest": "2.1.6"
       }
     },
     "node_modules/@babel/helper-string-parser": {
@@ -847,9 +847,9 @@
       }
     },
     "node_modules/nanoid": {
-      "version": "3.3.7",
-      "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.7.tgz",
-      "integrity": "sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g==",
+      "version": "3.3.8",
+      "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.8.tgz",
+      "integrity": "sha512-WNLf5Sd8oZxOm+TzppcYk8gVOgP+l58xNy58D0nbUnOxOWRWvlcCV4kUF7ltmI6PsrLl/BgKEyS4mqsGChFN0w==",
       "funding": [
         {
           "type": "github",
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));
+	});
+});