TypeScript

Identifiers

CategoryPattern
ConstantTitle_Case_Snake_Case
Class, Enum, Enum memberPascalCase
Interface, Type aliasIPascalCase
Type parameterPascalCase
Variable, Parameter, Function, Method, Property, Module aliascamelCase

Overall goal

An identifier should contain only upper case ASCII letters (U+0041 ~ U+005A), lower case ASCII letters (U+0061 ~ U+007A), ASCII digits (U+0030 ~ U+0039), hyphens (U+002D), and underscores (U+005F).

The rule for a kind of identifiers should add details upon generic common styles.

Constant

/^[A-Z][a-z0-9]*(_[A-Z][a-z0-9]*)+$/
1

Examples:

  • Non_Archived_Resources

  • Sidebar_Config

  • Slugify_Methods

  • Test_Workspace_Path

Description:

A constantopen in new window can be instantiated only once over the lifetime of the program, is intended to not be changed, and users must not modify it in any way.

The identifier of a constant use snake_case with the first letter of each word capitalized, and must be made of at least two words. We call it Title_Case_Snake_Case.

Background:

Enormous projects use UPPER_CASE_SNAKE_CASE, and even call it CONSTANT_CASE. However, we find it problematic, and suggest avoiding it. Generally speaking, mixed case is easiest to read and write, and all lower case is equally good, while all upper case is terrible. Constants are often used to hold important immutable values. When you propagate them across the codebase, significant negative visual effect of terrible styles will arise.

As for the "at least two words" requirement, we find in practice that the names of constants usually need two or three words to be clear and preciseopen in new window.

Class, Enum, Enum member

/^([A-Z][a-z0-9]+)+$/
1

Examples:

  • DownwardsArrowWithCornerLeftwards

  • EventEmitter

  • FontIcon

  • Lazy

Description:

Use PascalCase.

Interface, Type alias

/^I([A-Z][a-z0-9]+)+$/
1

Examples:

  • ICase

  • IDecorationAnalysisTask

  • IDecorationRecord

  • IDisposable

  • IDocumentToken

  • IInternalOption

  • IKnownKey

  • IMarkdownEngine

  • INlsBundle

  • IPrimitive

  • IWorkerRegistry

Description:

It's the same as class, but prefixed with I.

Background:

Someone advises against marking interfaces with special prefix, probably because interfaces are natural and frequent in JavaScript's structural type systemopen in new window.

We tried, but had a worse experience. The style helps to reduce distraction on small projects. But it soon becomes friction as the scale grows.

Interfaces are basically contracts that cannot be instantiated or contain any implementation. However, it's quite easy to forget this without a special mark. Too many times, some developers wondered why it's not allowed to new an object, and the codebase eventually revealed that the type is an interface.

To let it hit developers that they are not classes, you need to name interfaces nicelyopen in new window. From our experience, very few names can be intuitively recognized as "just a set of declarations of properties". Then, it goes back to how to mark interfaces.

Thus, we now think marking interfaces specially is good for engineering. We can discuss it as this style seems not consistent with other parts.

Type parameter

Description:

It's the same as class, but can also be single upper case letter.

Variable, Parameter, Function, Method, Property, Module alias

/^[a-z][a-z0-9]*([A-Z][a-z0-9]+)*$/
1

Examples:

  • applyDecoration

  • fs

  • inlineTokens

  • isWelcomeMessagesExist

  • onDidOpenTextDocument

  • parseInline

  • projectLevelOmittedHeadings

  • resolveResource

  • utf8Encoder

  • workers

Description:

Use camelCase.

Namespace, Decorator

Avoid.

Examples

The examples are for demonstration only. They are naive, and are never guaranteed to work.

// File: i-disposable.ts

export interface IDisposable {
    /**
     * Performs application-defined tasks associated with freeing, releasing, or resetting resources.
     */
    dispose(): any;
}
1
2
3
4
5
6
7
8
// File: text-writer.ts

import * as vscode from "vscode";
import { IDisposable } from "./i-disposable";
const utf8Encoder = new TextEncoder();

/**
 * Represents the state of a writer.
 */
export enum WriterState {
    Idle,
    Dirty,
    Disposed,
}

/**
 * Represents a writer that can write a sequential series of characters.
 */
export interface ITextWriter extends IDisposable {
    /**
     * Clears all buffers for the current writer and causes any buffered data to be written to the underlying stream.
     */
    flush(): void;

    /**
     * Writes a string to the text stream.
     */
    write(text: string): void;
}

/**
 * Default buffer size: 1024 bytes.
 */
const Default_Buffer_Size = 1024;

/**
 * Implements a naive text writer that appends text to a file.
 */
export class TextFileWriter implements ITextWriter {
    readonly #buffer: Uint8Array;

    readonly #file: vscode.Uri;

    /**
     * The next available index in the buffer.
     */
    #bufferRear = 0;

    #state: WriterState = WriterState.Idle;

    get file() {
        return this.#file;
    }

    get state() {
        return this.#state;
    }

    /**
     * @param file The file will be created, if it does not already exist.
     * @param bufferSize The buffer size in bytes.
     */
    constructor(file: vscode.Uri, bufferSize = Default_Buffer_Size) {
        this.#file = file;
        this.#buffer = new Uint8Array(bufferSize);
    }

    async dispose(): Promise<void> {
        if (this.#state === WriterState.Disposed) {
            return;
        }

        // Avoid throwing error.
        try {
            await this.flush();
        } catch {}

        this.#bufferRear = 0;
        this.#state = WriterState.Disposed;
    }

    #readFile(): Thenable<Uint8Array> {
        return vscode.workspace.fs.readFile(this.#file).then(
            (value) => value,
            (error) => {
                if (error instanceof vscode.FileSystemError && error.code === "FileNotFound") {
                    return new Uint8Array(0);
                } else {
                    throw error;
                }
            }
        );
    }

    async #writeFile(...data: readonly Uint8Array[]): Promise<void> {
        const totalLength = data.reduce((result, item) => result + item.length, 0);

        if (!totalLength) {
            throw new Error("`data` is empty.");
        }

        const content = new Uint8Array(totalLength);

        let ptr = 0;
        for (const item of data) {
            content.set(item, ptr);
            ptr += item.length;
        }

        await vscode.workspace.fs.writeFile(this.#file, content);
    }

    async flush(): Promise<void> {
        if (this.#bufferRear === 0) {
            return;
        }

        await this.#writeFile(
            await this.#readFile(), //
            this.#buffer.subarray(0, this.#bufferRear)
        );

        this.#bufferRear = 0;
        this.#state = WriterState.Idle;
    }

    async write(text: string): Promise<void> {
        const bin = utf8Encoder.encode(text);

        if (this.#bufferRear + bin.length >= this.#buffer.length) {
            await this.#writeFile(
                await this.#readFile(), //
                this.#buffer.subarray(0, this.#bufferRear),
                bin
            );

            this.#bufferRear = 0;
            this.#state = WriterState.Idle;
        } else {
            this.#buffer.set(bin, this.#bufferRear);
            this.#bufferRear += bin.length;
            this.#state = WriterState.Dirty;
        }
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
// File: dump.ts

import * as vscode from "vscode";
import { TextFileWriter } from "./text-writer";

export type ISerializablePrimitive = string | number | boolean | null;

/**
 * Represents an array that can be serialized to JSON.
 */
export type ISerializableArray = ReadonlyArray<ISerializablePrimitive | ISerializableObject>;

/**
 * Represents an object that can be serialized to JSON.
 */
export interface ISerializableObject {
    readonly [key: string]: ISerializablePrimitive | ISerializableObject | ISerializableArray;
}

/**
 * Serializes the data to JSON, and appends it to the file.
 *
 * @param path The Uri that points to the file.
 */
export async function dump<T extends ISerializableObject | ISerializableArray>(
    path: vscode.Uri,
    data: T
): Promise<void> {
    const writer = new TextFileWriter(path);
    try {
        await writer.write(JSON.stringify(data));
        await writer.flush();
    } finally {
        await writer.dispose();
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36