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
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
}
Array.prototype.map() of any[]
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 ?!
Array.fill
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
Typo on optional properties
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 ?!
Typescript types are "open", not "sealed" or "precise"
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
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
Keyof of union type is never
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 ?!
Here is the solution
type KeysOfUnion = T extends any ? keyof T: never;
type AvailableKeys = KeysOfUnion;
Spread operator of a typescript class
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
That's because Object spread only copies enumerable own properties...
Array.find over a (discriminated) union type
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[]'.
Here is another example:
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
Class implements interface
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)
}
}
That's all so far, feel free to write me what you think! :)
Top comments (0)