DEV Community

Manuele Dones
Manuele Dones

Posted on • Edited on

Typescript Pitfalls

Here is the my list of cases where the Typescript type system may resolve in unexpected results. They're mostly well documented on Github, but I often find bugs regarding the following collection.

Conditional Spread Operator in Object Literal with Optional props

TS playground

export interface MyModel {
  names: string[];
  age: number;
  address?: string
}

const myModel1: MyModel = {
  names: ['a', 'b', 'c'],
  age: 32,
  adress: 234234 // type here caught by TS since "excess property checking" is done
}

const myModel2: MyModel = {
  names: ['a', 'b', 'c'],
  age: 32,
  ...(true ? { adress: 234234 } : {}), // typo here NOT caught by TS, even if it's an Object literal
}
Enter fullscreen mode Exit fullscreen mode

Array.prototype.map() of any[]

TS Playground

interface MyInterface {
    id: number;
    name: string;
}
const myArray: any = [2, 3, 'hello', true];

const myInterfaces1: MyInterface[] = myArray.map(() => ({ wrong: 'prop' })); // fine for TS: what ?!

const myInterfaces2: MyInterface[] = myArray; // fine for TS: what ?!
Enter fullscreen mode Exit fullscreen mode

Array.fill

TS Playground

interface Cell {
  sign: 'X' | 'O';
  nested: {
    prop: string
  }
}

const state: Cell[] = Array(6).fill(0);

console.log(state[0].sign) // undefined
console.log(state[0].nested.prop) // cannot read property 'prop' of undefined
Enter fullscreen mode Exit fullscreen mode

Typo on optional properties

TS playground

interface World {
    c: boolean;
    address?: number;
}

const world: World = {
    c: true,
    adress: 234 //typo here caught by TS since "excess property checking" is done 
}

const worlds: World[] = [1, 2, 3].map(h => ({c: true, adress: 3})) // type here, but fine for TS: what ?!
Enter fullscreen mode Exit fullscreen mode

Typescript types are "open", not "sealed" or "precise"

TS playground

interface Person {
  first: string,
  last: string,
  middle?: string
}

function savePerson(person: Person) {
  return null;
}

const error = {
  first: 'Stefan',
  last: 'Baumgartner',
  midle: 'asdf' // typo here
}

savePerson(error); // error contains a typo, but it's fine for TS
Enter fullscreen mode Exit fullscreen mode

As you write functions, it’s easy to imagine that they will be called with arguments having the properties you’ve declared and no others. This is known as a 'sealed' or 'precise' type, and it cannot be expressed in TypeScript’s type system. Like it or not, your types are open.
Understand that JavaScript is duck typed and TypeScript uses structural typing to model this: values assignable to your interfaces might have properties beyond those explicitly listed in your type declarations. Types are not 'sealed'.
― Effective Typescript, Dan Vanderkam

Union Type

TS Playground
Union type is not a Discriminated Union Type
The OR (|) symbol may be misunderstood.

interface Bird {
  flySpeed: number;
}

interface Fish {
  swimSpeed: number
}

export type Birdish = Bird | Fish;

const birdish: Birdish = {
  flySpeed: 3,
  swimSpeed: 5
}

// that's why they called it "Union" Type
Enter fullscreen mode Exit fullscreen mode

Keyof of union type is never

TS Playground

interface Person {
  name: string;
}

interface Lifespan {
  birth: Date;
  death?: Date;
}
type PersonAndSpan = Person & Lifespan;
type PersonOrSpan = Person | Lifespan;

type PersonAndSpanKeys = keyof PersonAndSpan;  // 'name, 'birth', 'death'
type PersonOrSpanKeys = keyof PersonOrSpan;  // Type is never: what ?!
Enter fullscreen mode Exit fullscreen mode

Here is the solution
type KeysOfUnion = T extends any ? keyof T: never;
type AvailableKeys = KeysOfUnion;

Spread operator of a typescript class

TS Playground

class Foo {
    constructor(public a: number, public b: string) {
    }

    method() {
    }
}


function edit(foo: Foo) {
}

const foo: Foo = new Foo(2, 'foo');
const fooSpread = {...foo}

type keyofFoo = keyof typeof foo; // "a" | "b" | "method"
type keyofFooSpread = keyof typeof fooSpread; // "a" | "b"

edit(foo);
edit(fooSpread); // ERROR: Property 'method' is missing 
Enter fullscreen mode Exit fullscreen mode

That's because Object spread only copies enumerable own properties...

Array.find over a (discriminated) union type

TS Playground

interface A {
    a: number;
}

interface B {
    b: string;
}

type MixedType = (A | B)[]

const mixedType: MixedType = [{
    a: 3,
},
{
     b: 'string'
}];

const onlyA: A[] = mixedType.filter(m => 'a' in m); // error, Type '(A | B)[]' is not assignable to type 'A[]'.
Enter fullscreen mode Exit fullscreen mode

Here is another example:

TS Playground

interface Bird {
    fly(): void;
    layEggs(): void;
}

interface Fish {
    swim(): void;
    layEggs(): void;
}

type Animal = Bird | Fish;

function isFish(pet: Fish | Bird): pet is Fish {
    return (pet as Fish).swim !== undefined;
}

const animals: Animal[] = [{
    fly: () => { },
    layEggs: () => { }
}, {
    swim: () => { },
    layEggs: () => { }
}]

if (isFish(animals[0])) {
    const fish: Fish = animals[0]; // fish is typed as Fish, but...
}

const fish: Fish = animals.find(a => isFish(a)); // error, the results of find is Bird | Fish | undefined 
Enter fullscreen mode Exit fullscreen mode

Class implements interface

TS Playground

export interface User {
  prop: string | boolean;
}

export class UserClass implements User {
  prop = 'prop'

  constructor() {
    this.prop = true; // Type 'boolean' is not assignable to type 'string'.(2322)
  }

}
Enter fullscreen mode Exit fullscreen mode

That's all so far, feel free to write me what you think! :)

Top comments (0)