Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Operator to produce keys of a type where the properties of that type in those keys match some type #48992

Open
5 tasks done
RyanCavanaugh opened this issue May 6, 2022 · 8 comments
Labels
Awaiting More Feedback This means we'd like to hear from more people who would be helped by this feature Suggestion An idea for TypeScript

Comments

@RyanCavanaugh
Copy link
Member

RyanCavanaugh commented May 6, 2022

Suggestion

πŸ” Search Terms

keyof property keysOfType

βœ… Viability Checklist

My suggestion meets these guidelines:

  • This wouldn't be a breaking change in existing TypeScript/JavaScript code
  • This wouldn't change the runtime behavior of existing JavaScript code
  • This could be implemented without emitting different JS based on the types of the expressions
  • This isn't a runtime feature (e.g. library functionality, non-ECMAScript syntax with JavaScript output, new syntax sugar for JS, etc.)
  • This feature would agree with the rest of TypeScript's Design Goals.

⭐ Suggestion

We frequently get people writing definitions to produce the keys K of an object type O where O[K] extends T. Let's call this operation KeysOfType<O, T>. Varying userland definitions include

type ObjectKey<O, T> = {[K in keyof O]: O[K] extends T ? K : never}[keyof O & string]
// TODO: Link more definitions

These userland definitions work in concrete cases, but can't be used to write generic functions:

type ObjectKey<O, T> = {[K in keyof O]: O[K] extends T ? K : never}[keyof O & string];
function getString<O extends object, K extends ObjectKey<O, string>>(obj: O, key: K): string {
    // Can't assign ObjectKey<O, K> to 'string', but this is sound by construction
    return obj[key];
}

// Demo
interface MyInterface {
    someString: string;
    someNumber: number;
}
declare const mi: MyInterface;
getString(mi, "someString"); // OK
getString(mi, "someNumber"); // Error

We could add a new type operator or intrinsic alias KeysOfType<T, P> that returns all keyof T K such that T[K] extends P. When this type indexes a generic object of type T, we can then know that T[KeysOfType<T, P>] is assignable to P. In non-generic positions, this can be immediately resolved the same way as the userland definition.

type KeysOfType<O, P> = intrinsic;

// Hypothetical
function getString<O extends object>(obj: O, key: KeysOfType<O, string>): string {
     // OK, O[KeysOfType<O, string>] is known to be assignable to
     // string because O is a subtype of O and string is assignable to string
    return obj[key];
}

// getString works the same from the outside as the userland version

πŸ“ƒ Motivating Example

See getString above

πŸ’» Use Cases

This is a frequent complaint; need link more inbound issues here

#48989

@RyanCavanaugh RyanCavanaugh added Suggestion An idea for TypeScript Awaiting More Feedback This means we'd like to hear from more people who would be helped by this feature labels May 6, 2022
@jcalz
Copy link
Contributor

jcalz commented May 6, 2022

related: #30728

@RyanCavanaugh
Copy link
Member Author

Collecting more
#42795
#40654
#39972
#32550

@somebody1234
Copy link

Solution (to codeblock 2) - Playground

@jcalz
Copy link
Contributor

jcalz commented Apr 19, 2023

Note to self for future ease of searching: I usually call KeysOfType<O, T> by the name KeysMatching<T, V>

@RobertAKARobin
Copy link

RobertAKARobin commented Dec 28, 2023

Sure would be nice to see this implemented! The KeysMatching approach doesn't work when used on the base of a derived class, e.g.:

type KeysMatching<Target, Type> = {
	[Key in keyof Target]: Target[Key] extends Type ? Key : never
}[keyof Target];

class Animal {
	breathe() {
		console.log(`Ahh`);
	}

	exec(methodName: KeysMatching<this, Function>) {
		(this[methodName] as Function)();
	}
}

class Cat extends Animal {
	constructor() {
		super();
		this.exec(`meow`); // Argument of type 'string' is not assignable to parameter of type 'KeysMatching<this, Function>'.
	}

	meow() {
		console.log(`Meow`);
	}
}

@somebody1234
Copy link

@RobertAKARobin actually that doesn't even work on the base class.
This does though:
Playground Link

class Animal {
	breathe() {
		console.log(`Ahh`);
	}

	exec<T extends Record<MethodName, Function>, MethodName extends keyof T>(this: T, methodName: MethodName) {
		(this[methodName] as Function)();
	}
}

class Cat extends Animal {
    a = 1;

	constructor() {
		super();
		this.exec(`breathe`);
	}

	meow() {
		console.log(`Meow`);
	}
}

@Howard-Lam-UnitedVanning
Copy link

Howard-Lam-UnitedVanning commented Apr 19, 2024

Another issue that maybe related to this
In the following, in usetest2, Test2['e'] has the type never even though it should always be number. Or maybe the engine found out a case I never could think of.
EDIT: I have 2 implementations of BooleanKey here they both lead to the same results...

interface Test {
    a: boolean;
    b?: boolean;
    c: undefined;
    d?: string;
    e: number;
}
interface TestGeneric<T> extends Test {
    f: T;
    g: T[];
}
type BooleanKey<T, K extends keyof T = keyof T> = K extends any ? T[K] extends (boolean|undefined) ? (T[K] extends undefined ? never : K) : never : never;
type BooleanKeys<T> = {
    [K in keyof T]-?: T[K] extends (boolean | undefined) ? (T[K] extends undefined ? never : K) : never;
}[keyof T];
type t1 = BooleanKey<Test>;
type t2 = BooleanKeys<Test>;
type NonNullableBoolOnlyFields<T> = {
    [K in BooleanKeys<T>]-?: boolean;
};
type NonNullableBools<T> = T & NonNullableBoolOnlyFields<T>;
type Test1 = NonNullableBools<Test>
type Test2<T> = NonNullableBools<TestGeneric<T>>
type Field = 'e';

function test1(p1: Test1[Field]): number {
    const result: number = p1;
    return result;
}
function usetest1() {
    return test1(10);
}
function test2<T>(p1: Test2<T>[Field]): number {
    const result: number = p1;
    return result;
}
function usetest2<T>() {
    return test2<T>(10);
}

@jesseditson
Copy link

I'm having trouble trying to get the following to produce the correct type - is this the same issue as this discussion?

Playground

// This type allows defining "enums" which are either string values or are a single key with an embedded value (similar to Rust enums)
export type ValueEnum<K extends string = string, V = any> = {
  [P in K]: Record<P, V> & Partial<Record<Exclude<K, P>, never>> extends infer O
    ? { [Q in keyof O]: O[Q] }
    : never;
}[K];
export type Enum<T = any, K extends string = string> = K | ValueEnum<K, T>;

// This is an example of how you'd define an enum
export type ValidState<T> = ("unknown" | {
    "valid": T;
} | {
    "invalid": string;
} | "loading");

// I'm attempting to have unwrapEnum access embedded values directly (or return undefined if the enum is not of type K or type K is not an embedded type)
export type EnumData<T extends Enum> = T extends object ? T : never;
export type EnumDataKeys<T extends Enum> = T extends object ? keyof T : never;
export const unwrapEnum = <T extends Enum, const K extends EnumDataKeys<T>>(
  enm: EnumData<T> | string,
  type: K
) => {
  if (typeof enm === "string") {
    return undefined;
  }
  return enm[type];
};

// Example of code that uses the above ValidState code
export type InnerType = {
  test: string;
};

const test: ValidState<InnerType>  = { valid: { test: "FHHII" } };

// This produces InnerType
const validInfo1 = unwrapEnum(test, "valid");

const getValue = (): ValidState<InnerType> => {
  return { valid: { test: "FHHII" } };
}

// This produces "unknown"
const validInfo2 = unwrapEnum(getValue(), "valid");

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Awaiting More Feedback This means we'd like to hear from more people who would be helped by this feature Suggestion An idea for TypeScript
Projects
None yet
Development

No branches or pull requests

6 participants