sindresorhus / type-fest

A collection of essential TypeScript types
Creative Commons Zero v1.0 Universal
14.33k stars 544 forks source link

Add `Not<string, 'not me'>` #417

Open fregante opened 2 years ago

fregante commented 2 years ago

I was looking for a way to implement “any string that doesn’t start with a slash” with template literals, then I realized that it can be achieved with a generic Not type:

type Unslashed = Not<string, `/${string}`>
const valid: Unslashed = 'home'
const invalid: Unslashed = '/home'

Is there anything like Not?

Edit: related:

Upvote & Fund

Fund with Polar

sindresorhus commented 2 years ago

I think it could be useful.

If only TS had negated types: https://github.com/microsoft/TypeScript/pull/29317

fregante commented 2 years ago

Seen in https://stackoverflow.com/questions/66354585/typescript-type-for-any-string-other-than-a-specific-string-literal

I can't make it work as a reusable type:

type Not<Yes, Not> = Yes extends Not ? never : Yes;
type NotHello = Not<string, 'hello'>;
const notHello: NotHello = 'hello';
// No error because `NotHello === string`

It's possible that TypeScript "forgets" the negation too early

https://www.typescriptlang.org/play?#code/C4TwDgpgBAcg9sAPATQgZwDSwQPigXilTSggA9gIA7AExPmCgH4oqIA3CAJygC4j0AbgBQoSNmAAJCABsZcAhMRpgXAJZUA5lgDkAC1nydOEQGM4VFawTS5cfg1vzF+w3B2CgA


The generic-based works but it's buggy with variables:

type Not<Yes, Not> = Yes extends Not ? never : Yes;
type NotHello<S extends string> = Not<S, 'hello'>;

export const join = <S extends string>(
    ...parts: Array<NotHello<S> | number>
): string => parts.join(',');
join('hello'); // Error 🎉 
join('hello', 'world'); // Error 🎉 

let nonConstString = 'sup'
join('hello', nonConstString); // No error 😰 

https://www.typescriptlang.org/play?ts=4.6.4#code/C4TwDgpgBAcg9sAPATQgZwDSwQPigXilTSggA9gIA7AExPmCgH4oqIA3CAJygC4j0AbgCwAKFCRswABIQANnLiIAyqQrU6UNMC4BLKgHM8hBiqwByABbzF5nCNFjyYOF0YBjOFW1QAVnH0CKBU1SloSbT1DHAAKMQBIADpksABDNzR+AEEuLlSQRAZZBSVlPAAfVgBXAFsAI24cMQBKfkj9AwI8NIzE-30Y8wxzZod+qkHrEpHBKAB6OagAUVzXKEAeDcBI-agxccmbOCGocwB3VzkaGfnFla41rZ3HUTkIRiovAGEvbWUdDqDzGgqmBzLsAhMrAcju8qF9vMBflEDKNrthSKseIBeDcADHtQIA

kidonng commented 2 years ago

The generic-based works but it's buggy with variables

They needs to be as const or types will be widened.

Checkout the following code on TS Playground:

type Not<Yes, Not> = Yes extends Not ? never : Yes;
type NotHello<S extends string> = Not<S, 'hello'>;

export const join = <S extends string>(
    ...parts: Array<NotHello<S> | number>
): string => parts.join(',');
join('hello'); // Error 🎉 
join('hello', 'world'); // Error 🎉 

let nonConstString = 'sup' as const
join('hello', nonConstString); // Error 🎉 
tommy-mitchell commented 1 year ago

Could microsoft/TypeScript#51865 help solve this issue?