ryskajakub
ryskajakub

Reputation: 6431

typescript type conditionals extends object

I would like to have conditional type that something is an object.

type Id = {
    id: number
    obj: {
        x: 5
    }
}

type ObjString<T> = {
    [P in keyof T]:
        T[P] extends Object ? string : T[P]
}


const f: ObjString<Id> = {
    id: 4,
    obj: "xxxx"
}

here, the obj property is correctly mapped to string in the f object, but for the id property, I'll get this error message:

error TS2322: Type 'number' is not assignable to type 'string'.

That means that T[P] extends Object is evaluated to true for number. How should I construct the condition, so number will evaluate to false, whereas object would evaluate to true?

Upvotes: 1

Views: 1733

Answers (2)

jcalz
jcalz

Reputation: 330216

The Object type does not really correspond to non-primitives in TypeScript; rather it is the type of a values which can be indexed into like an object. This includes primitives like string and number, which get wrapped with objects when you index into them (and thus supports things like "hello".toUpperCase() and (123).toFixed(2)). Only null and undefined are not Objects in this sense. Object in TypeScript is rarely what you want.

If you are trying to find a type in TypeScript which means "non-primitive", you can use the object type instead (starts with a lowercase o instead of an uppercase O):

type ObjString<T> = {
  [P in keyof T]:
  T[P] extends object ? string : T[P]
}

type Z = ObjString<Id>;
/* type Z = {
    id: number;
    obj: string;
} */

const f: ObjString<Id> = {
  id: 4,
  obj: "xxxx"
}

And then everything works how you want here.

Keep in mind though that arrays and functions are also objects, so you might get some undesirable behavior depending on what you wanted to see there:

type Hmm = ObjString<{ foo(): string, bar: number[] }>;
// type Hmm = { foo: string; bar: string; }

Playground link to code

Upvotes: 2

This is because Object is too general type. Every primitive value in javascript extends Object because even numbers and string have their own methods. Consider this example:

type Check1 = string extends Object ? true : false // true
type Check2 = number extends Object ? true : false // true

I think it is better to approach the problem from other side. You can check whether type is primitive or not:

type Id = {
  id: number
  obj: {
    x: 5
  }
}

type Primitives = string | number | boolean | bigint | symbol;

type ObjString<T> = {
  [P in keyof T]:
  T[P] extends Primitives ? T[P] : string
}

const f: ObjString<Id> = {
  id: 4,
  obj: "xxxx"
}

Playground

Upvotes: 1

Related Questions