TypeScript for Java/C# Programmers
TypeScript is a popular choice for programmers accustomed to other languages with static typing, such as C# and Java.
TypeScript’s type system offers many of the same benefits, such as better code completion, earlier detection of errors, and clearer communication between parts of your program. While TypeScript provides many familiar features for these developers, it’s worth stepping back to see how JavaScript (and therefore TypeScript) differ from traditional OOP languages. Understanding these differences will help you write better JavaScript code, and avoid common pitfalls that programmers who go straight from C#/Java to TypeScript may fall in to.
If you’re familiar with JavaScript already but are primarily a Java or C# programmer, this introductory page can help explain some of the common misconceptions and pitfalls you might be susceptible to. Some of the ways that TypeScript models types are quite different from Java or C#, and it’s important to keep these in mind when learning TypeScript.
If you’re a Java or C# programmer that is new to JavaScript in general, we recommend learning a little bit of JavaScript without types first to understand JavaScript’s runtime behaviors. Because TypeScript doesn’t change how your code runs, you’ll still have to learn how JavaScript works in order to write code that actually does something!
It’s important to remember that TypeScript uses the same runtime as JavaScript, so any resources about how to accomplish specific runtime behavior (converting a string to a number, displaying an alert, writing a file to disk, etc.) will always apply equally well to TypeScript programs. Don’t limit yourself to TypeScript-specific resources!
C# and Java are what we might call mandatory OOP languages. In these languages, the class is the basic unit of code organization, and also the basic container of all data and behavior at runtime. Forcing all functionality and data to be held in classes can be a good domain model for some problems, but not every domain needs to be represented this way.
In JavaScript, functions can live anywhere, and data can be passed around freely without being inside a pre-defined class or struct . This flexibility is extremely powerful. “Free” functions (those not associated with a class) working over data without an implied OOP hierarchy tends to be the preferred model for writing programs in JavaScript.
Additionally, certain constructs from C# and Java such as singletons and static classes are unnecessary in TypeScript.
That said, you can still use classes if you like! Some problems are well-suited to being solved by a traditional OOP hierarchy, and TypeScript’s support for JavaScript classes will make these models even more powerful. TypeScript supports many common patterns such as implementing interfaces, inheritance, and static methods.
We’ll cover classes later in this guide.
TypeScript’s understanding of a type is actually quite different from C# or Java’s. Let’s explore some differences.
Nominal Reified Type Systems
In C# or Java, any given value or object has one exact type — either null , a primitive, or a known class type. We can call methods like value.GetType() or value.getClass() to query the exact type at runtime. The definition of this type will reside in a class somewhere with some name, and we can’t use two classes with similar shapes in lieu of each other unless there’s an explicit inheritance relationship or commonly-implemented interface.
These aspects describe a reified, nominal type system. The types we wrote in the code are present at runtime, and the types are related via their declarations, not their structures.
In C# or Java, it’s meaningful to think of a one-to-one correspondence between runtime types and their compile-time declarations.
In TypeScript, it’s better to think of a type as a set of values that share something in common. Because types are just sets, a particular value can belong to many sets at the same time.
Once you start thinking of types as sets, certain operations become very natural. For example, in C#, it’s awkward to pass around a value that is either a string or int , because there isn’t a single type that represents this sort of value.
In TypeScript, this becomes very natural once you realize that every type is just a set. How do you describe a value that either belongs in the string set or the number set? It simply belongs to the union of those sets: string | number .
TypeScript provides a number of mechanisms to work with types in a set-theoretic way, and you’ll find them more intuitive if you think of types as sets.
In TypeScript, objects are not of a single exact type. For example, if we construct an object that satisfies an interface, we can use that object where that interface is expected even though there was no declarative relationship between the two.
TypeScript’s type system is structural, not nominal: We can use obj as a Pointlike because it has x and y properties that are both numbers. The relationships between types are determined by the properties they contain, not whether they were declared with some particular relationship.
TypeScript’s type system is also not reified: There’s nothing at runtime that will tell us that obj is Pointlike . In fact, the Pointlike type is not present in any form at runtime.
Going back to the idea of types as sets, we can think of obj as being a member of both the Pointlike set of values and the Named set of values.
Consequences of Structural Typing
OOP programmers are often surprised by two particular aspects of structural typing.
The first is that the empty type seems to defy expectation:
TypeScript determines if the call to fn here is valid by seeing if the provided argument is a valid Empty . It does so by examining the structure of < k: 10 >and class Empty < >. We can see that < k: 10 >has all of the properties that Empty does, because Empty has no properties. Therefore, this is a valid call!
This may seem surprising, but it’s ultimately a very similar relationship to one enforced in nominal OOP languages. A subclass cannot remove a property of its base class, because doing so would destroy the natural subtype relationship between the derived class and its base. Structural type systems simply identify this relationship implicitly by describing subtypes in terms of having properties of compatible types.
Another frequent source of surprise comes with identical types:
ts
class Cardrive()// hit the gas>>class Golferdrive()// hit the ball far>>// No error?let w: Car = new Golfer();
Again, this isn’t an error because the structures of these classes are the same. While this may seem like a potential source of confusion, in practice, identical classes that shouldn’t be related are not common.
We’ll learn more about how classes relate to each other in the Classes chapter.
OOP programmers are accustomed to being able to query the type of any value, even a generic one:
csharp
// C#static void LogTypeT>()Console.WriteLine(typeof(T).Name);>
Because TypeScript’s type system is fully erased, information about e.g. the instantiation of a generic type parameter is not available at runtime.
JavaScript does have some limited primitives like typeof and instanceof , but remember that these operators are still working on the values as they exist in the type-erased output code. For example, typeof (new Car()) will be «object» , not Car or «Car» .
This was a brief overview of the syntax and tools used in everyday TypeScript. From here, you can:
The TypeScript docs are an open source project. Help us improve these pages by sending a Pull Request ❤
Объектно-ориентированное программирование
TypeScript реализует объектно-ориентированный подход, в нем есть полноценная поддержка классов. Класс представляет шаблон для создания объектов и инкапсулирует функциональность, которую должен иметь объект. Класс определяет состояние и поведение, которыми обладает объект.
Для определения нового класса применяется ключевое слово class . Например, определим класс User:
После определения класса мы можем создавать его объекты:
let tom: User = new User(); let alice = new User();
Здесь определено два объекта класса User — tom и alice.
Поля класса
Для хранения состояния объекта в классе определяются поля:
Здесь определены два поля — name и age , которые имеют типы string и number соответственно. Фактически поля представляют переменные уровня класса, только для их объявления не применяются var и let .
По имени объекта мы можем обращаться к этим полям:
class User < name: string; age: number; >let tom = new User(); tom.name = "Tom"; tom.age = 36; console.log(`name: $ age: $`); // name: Tom age: 36
При определении полей им можно задать некоторые начальные значения:
class User < name: string = "Tom Smith"; age: number = 18; >let user = new User(); console.log(`name: $ age: $`); // name: Tom Smith age: 18
Методы
Классы также могут определять поведение — некоторые действия, которые должны выполнять объекты этого класса. Для этого внутри класса определяются функции, которые называются методами.
class User < name: string; age: number; print()< console.log(`name: $age: $`); > toString(): string< return `$: $`; > >
Здесь в классе User определены два метода. Метод print() выводит информацию об объекте на консоль, а метод toString() возвращает некоторое представление объекта в виде строки.
В отличие от обычных функций для определения методов не указывается ключевое слово function .
Для обращения внутри методов к полям и другим методам класса применяется ключевое слово this , которое указывает на текущий объект этого класса.
class User < name: string; age: number; print()< console.log(`name: $age: $`); > toString(): string< return `$: $`; > > let tom = new User(); tom.name = "Tom"; tom.age = 36; tom.print(); // name: Tom age: 36 console.log(tom.toString()); // Tom: 36
Конструкторы
Кроме обычных методов классы имеют специальные функции — конструкторы, которые определяются с помощью ключевого слова constructor . Конструкторы выполняют начальную инициализацию объекта. Например, добавим в класс User конструктор:
class User < name: string; age: number; constructor(userName: string, userAge: number) < this.name = userName; this.age = userAge; >print() < console.log(`name: $age: $`); > > let tom = new User("Tom", 36); tom.print(); // name: Tom age: 36
Здесь конструктор применимает два параметра и использует их значения для установки значения полей name и age:
constructor(userName: string, userAge: number)
Затем при создании объекта в конструктор передается два значения для его параметров:
Поля для чтения
Полям класса в процессе работы программы можно присваивать различные значения, которые соответствуют типу полей. Однако TypeScript также позволяет определять поля для чтения, значения которых нельзя изменить (кроме как в конструкторе). Для определения таких полей применяется ключевое слово readonly :
Значение полей для чтения можно установить либо при объявлении:
readonly name: string = "Default user";
constructor(userName: string, userAge: number)
В остальных местах программы значение этого поля нельзя изменить. Например, в следующем случае мы получим при компиляции ошибку, потому что пытаемся установить поле для чтения:
class User < readonly name: string = "Default user"; age: number; constructor(userName: string, userAge: number) < this.name = userName; this.age = userAge; >print() < console.log(`name: $age: $`); > > let tom = new User("Tom", 36); tom.name = "Bob"; // ! Ошибка - поле name - только для чтения tom.print(); // name: Tom age: 36