Skip to content

TS3: Objects

Chapter 10: Objects

  • JavaScript objects are collections of properties that can be created using the literal syntax, constructor functions, or classes.
  • TypeScript focuses on an object’s “shape,” which is the combination of its property names and types.
  • To match a type, an object must define all the properties in the shape (its type annotation).
  • The compiler will still match an object if it has additional properties that are not defined by the shape type

Excess Properties

  • Excess properties are additional properties that are not defined by the type.
type XY = {
    x: number;
    y: number;
};
const x6 = {
    x: 1,
    y: 2,
};

const y6: XY = {
    x: 1,
    y: 2,
    z: 3,
}; // compiler error, Object literal may only specify known properties, and 'z' does not exist in type 'XY'.

const y6_2 = {
    x: 1,
    y: 2,
    z: 3,
}; // not an error, since the y_2 object has no type annotation XY

const all: XY[] = [x6, y6, y_2]; // works, since all elements define all of the properties required by the type XY
  • The compiler reports errors when object literals with type annotations define additional properties, because this is likely to be a mistake (y6 in the example above).
  • Excess properties do not cause errors when an object is defined without a type annotation (y6_2 in the example above).
  • you can use compiler option suppressExcessPropertyErrors to suppress errors related to excess properties.

Type Unions

  • type unions = extract shared properties into a new type
type Person = {
    id: number;
    name: string;
    age: number;
};

type Product = {
    id: number;
    name: string;
    price: number;
};

type PersonProductTypeUnion = Person | Product; // union type

const arr: PersonProductTypeUnion[] = [
    {
        id: 1,
        name: "product1",
        price: 100,
    },
    {
        id: 1,
        name: "person1",
        age: 100,
    },
];

arr.forEach((pp) => {
    console.log(pp.id);
    console.log(pp.name);
    // above this stage, only name and id are available since these are the shared properties of Person and Product

    console.log(pp.age); //Error: Property 'age' does not exist on type 'PersonProductTypeUnion'.
    // Property 'age' does not exist on type 'Product'

    // type guard by checking a property
    if ("age" in pp) {
        // type now is: Person
        console.log(pp.age);
    } else {
        // type now is: Product
        console.log(pp.price);
    }
});

Type Guards

  • type guards are used to check that an object of a certain type is being used.
  • for example, the PersonProductTypeUnion above can have either a Person or Product type. we need to confirm that the object is of the correct type before accessing the properties.

Type Guarding by Checking Properties

  • use in keyword to check that a discriminated (distinguishable) property exists on an object. eg. if("age" in obj){ /** obj is of type Person **/ }

Type Guarding with a Type Predicate Function

  • TypeScript also supports guarding object types using a function.
function isPerson(obj: PersonProductTypeUnion): obj is Person {
    return "age" in obj;
}

Type Intersections

  • Type intersections combine the features of multiple types, allowing all the features to be used.
  • it is the opposite of type unions, which remove the features that are not shared between its types.
  • type intersections = type merge.
type Person = {
    id: number;
    name: string;
    age: number;
};

type Employee = {
    department: string;
    salary: number;
    startDate: Date;
};

type PersonEmployeeIntersection = Person & Employee; // intersection type

const me: PersonEmployeeIntersection = {
    // must define all properties of Person and Employee
    id: 1,
    name: "person1",
    age: 100,
    department: "IT",
    salary: 100,
    startDate: new Date(),
};
  • the object that results from the intersection type can fit into any of the types that are combined. eg. PersonEmployeeIntersection can be used as a Person or Employee object.

problem when type intersections have the same property name

  • when the intersected types have property with the same name, the type of the newly intersected property is the intersection between the types of this property, but in JS these properties will override each other.
type Person = {
    id: number;
    name: string;
    age: number;
};

type Employee = {
    id: string;
    department: string;
    salary: number;
    startDate: Date;
};

type PersonEmployeeIntersection = Person & Employee; // intersection type

// compiler will compile the intersection type as:
type PersonEmployeeIntersection = {
    id: number & string; // the property that has the issue since, number & string is impossible
    name: string;
    age: number;
    department: string;
    salary: number;
    startDate: Date;
};
  • the compiler will raise an error in such cases if the properties with the shared name are of primitive types.
const p: TPerson = {
    id: 1,
    name: "person1",
    age: 100,
};

const e: Employee = {
    id: "1",
    department: "IT",
    salary: 100,
    startDate: new Date(),
};

// Error: type '{ id: string; department: string; salary: number; startDate: Date; name: string; age: number; }'
//      is not assignable to type 'PersonEmployeeIntersection'.
// Type '{ id: string; department: string; salary: number; startDate: Date; name: string; age: number; }'
//      is not assignable to type 'TPerson'.
// Types of property 'id' are incompatible.  Type 'string' is not assignable to type 'number'.ts(2322)
const p_e: PersonEmployeeIntersection = {
    ...p,
    ...e,
};
  • but if the properties with the shared name are of type objects, the compiler may not raise any errors.
type TTPerson = {
    id: { userId: number };
    name: string;
    age: number;
};

type TTEmployee = {
    id: { userId: number; contractId: string };
    department: string;
    salary: number;
    startDate: Date;
};

const ttp: TTPerson = {
    id: { userId: 1 },
    name: "person1",
    age: 100,
};

const tte: TTEmployee = {
    id: { userId: 2, contractId: "1" },
    department: "IT",
    salary: 100,
    startDate: new Date(),
};

const ttp_tte: TTPerson & TTEmployee = {
    ...ttp,
    ...tte,
};

console.log({ ttp_tte }); // id from tte will `completely override` id from ttp, and the compiler won't raise any errors.
/**
{
  ttp_tte: {
    id: { userId: 2, contractId: '1' },
    name: 'person1',
    age: 100,
    department: 'IT',
    salary: 100,
    startDate: 2022-08-29T10:24:31.982Z
  }
}
*/
  • in the example above, id from tte will completely override id from ttp, and the compiler won't raise any errors.

Fix problem when type intersections have the same property name

  • these errors are very hard to debug and detect, and the best solution is to:
    • avoid using type intersections if there are properties with shared names
    • when merging use a function that intersect the 2 objects properly, eg. rename the property in question, and use the type returned by this intermediate function.
  • the solution may be useful if you manually intersect types, but with OOP style, inheritance may cause this to happen.