Home > Mobile >  Typescript type with prohibited properties names
Typescript type with prohibited properties names

Time:08-04

I need to create a type which excludes certain literal types and accepts every other string. I tried this:

type ExcludedKeys = "a"|"b"

type MyType = {
    [K in Exclude<string,ExcludedKeys>]: any
}

const obj: MyType = {
    a: 0, // No Error
    b: 1 // No Error
}

But soon I found out that Exclude<string,ExcludedKeys> simply evaluates to string and it's not possible to do it this way. Then I have tried this approach:

type ExcludedKeys = "a"|"b"

type MyType<T> = keyof T extends ExcludedKeys ? never : {
    [K in keyof T]: T[K]
} 

declare class Obj {
    a: number
    b: number
    c: number // Adding this removes the wanted error.
}

const obj: MyType<Obj> = {
    a: 0, // No Error
    b: 1, // No Error
    c: 3
}

but this only works when members of ExcludedKeys are the only props of the object.

TS Playground link

What I need

As said, a type that negates those property names assignable to a set of string

type ExcludedKeys = "a"|"b"

const obj = {
    a: 0, // Error Here
    b: 1, // Error Here
    c: 3
}

Edit

Even though I didn't mention it to simplify the context, as jsejcksn's answer pointed out, I needed this type to preserve type info from a given class model. With that said, lepsch's answer remains the accepted one because it does just what I asked for in the most short and simple way. Anyway, I'd like to share how I changed that approach to suit my needs.

type ExcludedKeys = "a"|"b"

type MyType<T> = {
    [K in keyof T]: T[K]
} & {
    [K in ExcludedKeys]?: never
}

Playground link

CodePudding user response:

You're almost there. Try this:

type ExcludedKeys = "a"|"b"

type MyType1 = {
    [key: string]: any
} & {
    [K in ExcludedKeys]: never
}

const obj1: MyType1 = {
    a: 0, // Error
    b: 1, // Error
    c: 3, // No Error
}

CodePudding user response:

First, some context:

The code in another answer will help you avoid assigning values to the excluded keys using only a type annotation, but you'll lose type information for the other properties:

TS Playground

type ExcludedKeys = "a"|"b"

type MyType1 = {
  [key: string]: any
} & {
  [K in ExcludedKeys]: never
}

const obj1: MyType1 = {
  a: 0, // Error
  b: 1, // Error
  c: 3, // No Error
}

obj1.a
   //^? (property) a: never
obj1.b
   //^? (property) b: never
obj1.c
   //^? any (this should be `number`)
obj1.d
   //^? any (oops, this doesn't actually exist)
// ...etc.

To prevent the excluded keys and preserve type information for the other properties of your value, you can use a generically-constrained identity function:

TS Playground

type ExcludedKeys = "a" | "b";

function createMyValue <
  T extends Record<string, any> & Partial<Record<ExcludedKeys, never>>,
>(value: T): T {
  return value;
}

createMyValue({
  a: 'hello', /*
  ~
  Type 'string' is not assignable to type 'never'.(2322) */
  b: 1, /*
  ~
  Type 'number' is not assignable to type 'never'.(2322) */
  c: true,
});

const obj = createMyValue({
  c: true,
  d: 'hello',
  e: 2,
});

obj.c
  //^? (property) c: boolean
obj.d
  //^? (property) d: string
obj.e
  //^? (property) e: number
obj.f /*
    ~
Property 'f' does not exist on type '{ c: boolean; d: string; e: number; }'.(2339) */

CodePudding user response:

You can map the keys of a type and check if a key extends your ExcludedKeys. Here's a utility type for this:

type ExcludedKeys = 'a' | 'b';

type ExcludeKeys<T extends object, ToExlude> = {
  [K in keyof T]: K extends ToExlude ? never : any;
};

declare class Obj  {
    a: number;
    b: number;
    c: number;
};

const obj: ExcludeKeys<Obj, ExcludedKeys> = {
  a: 0, // Type 'number' is not assignable to type 'never'
  b: 0, // Type 'number' is not assignable to type 'never'
  c: 0, // works
};

CodePudding user response:

You can add the forbidden keys as extra optional properties of type undefined to the type:

type ExcludedKeys = "a"|"b"


type ExcludeKeysFrom<T extends object, ToExlude extends PropertyKey> = Omit<T, ToExlude> & Partial<Record<ToExlude, undefined>>

declare class Obj  {
    a: number;
    b: number;
    c: number;
};

// erro as expected
const obj: ExcludeKeysFrom<Obj, ExcludedKeys> = {
  a: 0, // Type 'number' is not assignable to type 'never'
  b: 0, // Type 'number' is not assignable to type 'never'
  c: 0, // works
};

// ok
const obj2: ExcludeKeysFrom<Obj, ExcludedKeys> = {
  c: 0, // works
};

Playground Link

Why optional? So that they don't need to present in the object. Why undefined and not never because never is assignable to any other type so if you were to use never the resulting object gains all the forbidden properties which you can access and which will be assignable to anything (ex)

CodePudding user response:

Even though I didn't mention it to simplify the context, as jsejcksn's answer pointed out, I needed this type to preserve type info from a given class model. With that said, lepsch's answer remains the accepted one because it does just what I asked for in the most short and simple way. Anyway, I'd like to share how I changed that approach to suit my needs.

type ExcludedKeys = "a"|"b"

type MyType<T> = {
    [K in keyof T]: T[K]
} & {
    [K in ExcludedKeys]?: never
}

Playground link

  • Related