To create an object from an interface in TypeScript, we write an object literal that has every property the interface requires and give the variable the interface as its type. For example, const lokesh: User = { name: “Lokesh”, age: 37 } creates a User object. We cannot call new User(), because an interface is only a compile-time description of a shape and does not exist in the compiled JavaScript. Any object with the right properties, however it was built, is a valid User.
We create objects from interfaces for most data in a TypeScript app, such as the user in a login response or the settings object of a page. In Java, an interface cannot be instantiated, and we need a class (or an anonymous class or lambda) to get an object. In TypeScript, a class is only one of several options, and each of the following sources can produce objects of an interface type.
- An object literal
- A copy of another object
- A factory function
- A class
- Data parsed from JSON
The following example creates User objects in five common ways, ordered from the most used to the least, compiled with TypeScript 7.0.2 and run on Node.js 22. The compiler checks each result against the same User interface.
interface User {
name: string;
age: number;
greet(): string;
}
// 1. Object literal with a type annotation
const lokesh: User = {
name: "Lokesh",
age: 37,
greet() { return "Hi, I am " + this.name; },
};
// 2. Object literal checked with satisfies
const raj = {
name: "Raj",
age: 35,
greet() { return "Hello from " + this.name; },
} satisfies User;
// 3. Copy of an existing object, with one change
const older: User = { ...lokesh, age: 38 };
// 4. Factory function with a default value
function createUser(name: string, age = 18): User {
return { name, age, greet() { return "Hi, I am " + this.name; } };
}
const john = createUser("John", 40);
// 5. Class that implements the interface
class Member implements User {
name: string;
age: number;
constructor(name: string, age: number) {
this.name = name;
this.age = age;
}
greet(): string { return "Hi, I am " + this.name; }
}
const brian = new Member("Brian", 25);
const g1 = lokesh.greet(); // g1 = "Hi, I am Lokesh"
const g2 = raj.greet(); // g2 = "Hello from Raj"
const age = older.age; // age = 38
const g3 = john.greet(); // g3 = "Hi, I am John"
const g4 = brian.greet(); // g4 = "Hi, I am Brian"
Notice that only option 5 uses new, and it calls new on the class Member, never on the interface.
Next, we see why new fails on an interface and go through each way of creating the object. Along the way, we look at the satisfies operator and at two common mistakes, empty objects created with {} as User and shallow copies made with the spread operator.
1. Why new Does Not Work With an Interface
The new operator calls a constructor function at runtime. The TypeScript compiler removes every interface from its output, so there is no User function to call. Writing new User() stops the build with error TS2693.
error TS2693: 'User' only refers to a type, but is being used as a value here.
The same error appears whenever an interface name is used where a value is expected, for example with instanceof. The fix is to create a plain object and let the compiler compare its shape with the interface. If we need a constructor, we write a class that implements the interface, as shown in section 7.
2. Object Literal With a Type Annotation
An object literal is a list of properties inside curly braces. With the annotation : User after the variable name, the compiler checks the literal against the interface in several ways.
- Every required property is present.
- Each value has the right type.
- The literal has no properties the interface does not declare.
Optional properties may be left out.
interface User {
name: string;
age: number;
email?: string;
greet(): string;
}
// 1. Method syntax: this is the object
const lokesh: User = {
name: "Lokesh",
age: 37,
greet() { return "Hi, I am " + this.name; },
};
// 2. Arrow function: it has no own this
const raj: User = {
name: "Raj",
age: 35,
greet: () => "Hi, I am Raj",
};
const g1 = lokesh.greet(); // g1 = "Hi, I am Lokesh"
const g2 = raj.greet(); // g2 = "Hi, I am Raj"
const email = lokesh.email; // email = undefined
The two objects differ in how they implement greet(). With the method syntax, this refers to the object, so the method can read this.name. An arrow function does not get its own this, so it cannot read the other properties that way. For methods that use the object’s data, we use the method syntax.
We create most objects with an annotated literal, because it needs no class and the errors point at the property that is missing or wrong. For example, a unit test builds the User it needs in one statement, and the compiler flags the test data as soon as the interface gets a new required property.
3. Checking an Object With satisfies
The satisfies operator, available since TypeScript 4.9, runs the same checks as an annotation, including the check for extra properties. The difference is the type of the variable. An annotation gives the variable the interface type, whereas with satisfies the variable keeps the type that TypeScript infers from the literal, which is often more precise.
interface User {
name: string;
age: number;
email?: string;
}
// 1. Annotation: the type is User, so email may be undefined
const lokesh: User = { name: "Lokesh", age: 37, email: "lokesh@example.com" };
const domain1 = lokesh.email?.split("@")[1]; // domain1 = "example.com"
// 2. satisfies: checked against User, email is known to be a string
const raj = { name: "Raj", age: 35, email: "raj@example.com" } satisfies User;
const domain2 = raj.email.split("@")[1]; // domain2 = "example.com", no ?. needed
For lokesh, the variable type is User, so email has the type string | undefined, and we need the optional chaining operator ?.. For raj, the compiler knows email is a string. We use satisfies for constant objects that are written once and read many times, such as configuration and lookup tables. We use an annotation when the variable is reassigned later or passed around as a general User.
4. Type Assertions With as and Building an Object Step by Step
A type assertion, {} as User, makes the compiler treat a value as a User without checking that it has the properties. The assertion compiles, but the object is still empty at runtime. The compiler types broken.name as a string, so it never warns us about the undefined value.
interface User {
name: string;
age: number;
}
// 1. Type assertion: compiles, but the object is empty
const broken = {} as User;
const name = broken.name; // name = undefined, typed as string
// 2. Partial<User> while the object is being filled
const draft: Partial<User> = {};
draft.name = "John";
draft.age = 40;
// 3. Check before treating it as a User
if (draft.name !== undefined && draft.age !== undefined) {
const john: User = { name: draft.name, age: draft.age };
console.log(john); // { name: "John", age: 40 }
}
When an object is filled in several steps, Partial<User> is the correct type. It marks every property as optional, and the compiler makes us check each one before we build the final User. For example, a two-page sign-up form fills the name on the first page and the age on the second.
An assertion still has a place when we know more than the compiler, for example for a value that a test framework fills in. The compiler blocks assertions between types that do not overlap at all, such as { nam: “Raj” } as User, with error TS2352: Conversion of type ‘{ nam: string; }’ to type ‘User’ may be a mistake.
5. Copying an Existing Object With Spread and structuredClone()
The spread operator … copies the properties of one object into a new object literal. Properties written after the spread replace the copied ones, so { …lokesh, age: 38 } is a copy of lokesh with a new age. A spread copy is the common way to “change” an object without modifying the original, for example when a settings page saves a new value and keeps the old settings for an undo button.
interface Address {
city: string;
}
interface User {
name: string;
age: number;
address: Address;
}
const lokesh: User = { name: "Lokesh", age: 37, address: { city: "Delhi" } };
// 1. Copy and change one property
const older: User = { ...lokesh, age: 38 };
const oldAge = lokesh.age; // oldAge = 37, original unchanged
// 2. Spread copies one level only: address is shared
older.address.city = "Pune";
const city = lokesh.address.city; // city = "Pune"
// 3. Deep copy with structuredClone()
const clone: User = structuredClone(lokesh);
clone.address.city = "Agra";
const city2 = lokesh.address.city; // city2 = "Pune"
The second step shows the problem. Spread copies only the top-level properties, so older.address and lokesh.address point to the same object. Changing the city through one variable changes it for both.
The function structuredClone() copies nested objects as well. It works for data objects only, and an object that contains a function, such as a greet() method, makes it throw a DataCloneError.
When the source object has more properties than the interface, the copy compiles as long as the source is a variable and not a literal, because the excess property check applies only to object literals. The extra properties are still copied into the new object at runtime.
6. Factory Function With Default Values
A factory function is a plain function that returns a new object, and it does the job that a static factory method does in Java. The return type User makes the compiler check the object, and the function hides how the object is built.
A common pattern takes a Partial<User> argument and spreads it over a set of defaults, so the caller passes only the properties that differ from the defaults.
interface User {
name: string;
age: number;
active: boolean;
}
// Defaults first, caller values override them
function createUser(name: string, overrides: Partial<User> = {}): User {
return { name, age: 18, active: true, ...overrides };
}
const john = createUser("John"); // { name: "John", age: 18, active: true }
const raj = createUser("Raj", { age: 35 }); // { name: "Raj", age: 35, active: true }
Factories with defaults are common in tests, where many objects need the same values except for one or two fields. The = {} part is a default parameter value, so callers can leave out overrides.
7. Class That Implements the Interface
A class with implements User produces objects that match the interface, and the compiler checks the class once instead of every object. A class is the right choice when the objects need at least one class feature.
- A constructor
- Private state
- Several methods that work on the same data
- A runtime type that instanceof can test
interface User {
name: string;
age: number;
greet(): string;
}
class Member implements User {
name: string;
age: number;
constructor(name: string, age: number) {
this.name = name;
this.age = age;
}
greet(): string {
return "Hi, I am " + this.name;
}
// Extra method, not part of User
birthday(): void {
this.age++;
}
}
const raj = new Member("Raj", 35);
raj.birthday();
const age = raj.age; // age = 36
const user: User = raj; // a Member is a User
const text = user.greet(); // text = "Hi, I am Raj"
const isMember = raj instanceof Member; // isMember = true
The class may have more members than the interface, such as birthday(). When we hold the object through a User variable, only the interface members are visible. The test raj instanceof Member works because Member exists at runtime; raj instanceof User would not compile. An interface exists only at compile time, whereas a class also exists at runtime, which is the main difference between an interface and a class.
8. Creating Objects From JSON Data
Data from an API or a file arrives as a JSON string, and JSON.parse() returns the type any, which can be assigned to a User variable without any check. So the annotation describes what we expect, not what we received.
interface User {
name: string;
age: number;
}
const json = '{"name":"Lokesh","age":37}';
// 1. JSON.parse() returns any; the annotation is not checked at runtime
const lokesh: User = JSON.parse(json);
const age = lokesh.age; // age = 37
// 2. A runtime check before using the data
function isUser(value: unknown): value is User {
return typeof value === "object" && value !== null
&& typeof (value as User).name === "string"
&& typeof (value as User).age === "number";
}
const data: unknown = JSON.parse('{"name":"Raj"}');
const valid = isUser(data); // valid = false, age is missing
For data that our own code wrote, the annotation is enough. For data from outside the program, we check it first. For example, a weather widget that reads a third-party API can get a payload with a missing field after the API changes.
The isUser() function is a user-defined type guard. Its return type value is User makes the compiler treat data as a User inside an if (isUser(data)) block. Type guards like isUser() take the place of instanceof, which cannot check an interface. For large payloads, a schema validation library saves writing these checks by hand.
9. Which Way to Use
Each technique produces an object that the compiler accepts as a User. They differ in how much the compiler checks and in what exists at runtime.
| Technique | Compiler checks the properties | Good for |
|---|---|---|
| Object literal with : User | Yes, including extra properties | Most objects |
| Object literal with satisfies User | Yes, keeps the exact literal types | Constants and configuration |
| {} as User | No | Rare cases where we know more than the compiler |
| Spread copy { …user } | Yes | Changing a copy, not the original |
| Factory function | Yes, through the return type | Default values, test data |
| Class with implements | Yes, once for the class | Constructors, private state, instanceof |
| JSON.parse() | No, the result is any | External data, after a runtime check |
10. Getting the Complete Source Code
The create-object-from-interface project has one file for each section and runs them in order. It compiles with TypeScript 7.0.2 and runs on Node.js 22 or newer, which provides structuredClone().
npm install
npm start
The command npm start prints every value from the snippet comments.
11. Conclusion
We cannot instantiate a TypeScript interface, but we can create any number of objects that match it. An object literal with a type annotation covers most cases, and satisfies keeps the exact types for constants. A factory function adds default values, whereas a class is the choice when we need a constructor or a runtime type. The two common mistakes are {} as User, which skips every check, and the spread operator, which copies only the first level of an object.
12. References
The TypeScript Handbook covers annotations, assertions and type predicates, and MDN describes how spread syntax and structuredClone() copy objects at runtime.
- TypeScript Handbook: Object Types
- TypeScript 4.9 release notes: the satisfies operator
- TypeScript Handbook: Type Assertions
- TypeScript Handbook: Using Type Predicates
- MDN: Spread syntax
- MDN: structuredClone()
Happy Learning !!